Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support completion of external targets #307

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -167,6 +167,11 @@
"default": false,
"markdownDescription": "Whether to add a CodeLens to `BUILD`/`BUILD.bazel` files to provide actions while browsing the file."
},
"bazel.enableExternalTargetCompletion": {
"type": "boolean",
"default": true,
"markdownDescription": "Whether to enable IntelliSense code completion for external targets in BUILD files."
},
"bazel.pathsToIgnore": {
"type": "array",
"items": {
28 changes: 21 additions & 7 deletions src/bazel/bazel_utils.ts
Original file line number Diff line number Diff line change
@@ -19,18 +19,16 @@ import { blaze_query } from "../protos";
import { BazelQuery } from "./bazel_query";

/**
* Get the targets in the build file
* Get the package label for a build file.
*
* @param bazelExecutable The path to the Bazel executable.
* @param workspace The path to the workspace.
* @param buildFile The path to the build file.
* @returns A query result for targets in the build file.
* @returns The package label for the build file.
*/
export async function getTargetsForBuildFile(
bazelExecutable: string,
export function getPackageLabelForBuildFile(
workspace: string,
buildFile: string,
): Promise<blaze_query.QueryResult> {
): string {
// Path to the BUILD file relative to the workspace.
const relPathToDoc = path.relative(workspace, buildFile);
// Strip away the name of the BUILD file from the relative path.
@@ -43,7 +41,23 @@ export async function getTargetsForBuildFile(
// Change \ (backslash) to / (forward slash) when on Windows
relDirWithDoc = relDirWithDoc.replace(/\\/g, "/");
// Turn the relative path into a package label
const pkg = `//${relDirWithDoc}`;
return `//${relDirWithDoc}`;
}

/**
* Get the targets in the build file
*
* @param bazelExecutable The path to the Bazel executable.
* @param workspace The path to the workspace.
* @param buildFile The path to the build file.
* @returns A query result for targets in the build file.
*/
export async function getTargetsForBuildFile(
bazelExecutable: string,
workspace: string,
buildFile: string,
): Promise<blaze_query.QueryResult> {
const pkg = getPackageLabelForBuildFile(workspace, buildFile);
const queryResult = await new BazelQuery(
bazelExecutable,
workspace,
95 changes: 95 additions & 0 deletions src/completion-provider/bazel_repository_completion_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2023 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as vscode from "vscode";
import { queryQuickPickTargets } from "../bazel";

function isCompletingInsideRepositoryLabel(
document: vscode.TextDocument,
position: vscode.Position,
) {
const linePrefix = document
.lineAt(position)
.text.substring(0, position.character);
const startOfRepo = linePrefix.lastIndexOf("@");
const endOfRepo = linePrefix.lastIndexOf("//");
return startOfRepo !== -1 && (endOfRepo === -1 || endOfRepo < startOfRepo);
}

function getTargetName(label: string) {
const colonIndex = label.lastIndexOf(":");
if (colonIndex === -1) {
return undefined;
}
return label.substring(colonIndex + 1);
}

export class BazelRepositoryCompletionItemProvider
implements vscode.CompletionItemProvider {
private repositories?: Promise<string[]>;

/**
* Returns completion items matching the given prefix.
*/
public async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
) {
const bazelConfig = vscode.workspace.getConfiguration("bazel");
const enableExternalTargetCompletion = bazelConfig.get<boolean>(
"enableExternalTargetCompletion",
);
if (!enableExternalTargetCompletion) {
return [];
}

if (!isCompletingInsideRepositoryLabel(document, position)) {
return [];
}

const repos = await this.getRepos();
const completionItems = repos.map(
(repo) =>
new vscode.CompletionItem(repo, vscode.CompletionItemKind.Folder),
);
return completionItems;
}

/**
* Runs a bazel query command to acquire all the repositories in the
* workspace.
*/
public async refresh(): Promise<void> {
await this.queryAndCacheRepos();
}

private async getRepos(): Promise<string[]> {
if (this.repositories) {
return await this.repositories;
}
return await this.queryAndCacheRepos();
}

private async queryAndCacheRepos(): Promise<string[]> {
const queryRepos = async () => {
const targets = await queryQuickPickTargets(
"kind('.* rule', //external:*)",
);
return targets.map((target) => getTargetName(target.label));
};
const deferred = queryRepos();
this.repositories = deferred;
return await deferred;
}
}
Original file line number Diff line number Diff line change
@@ -13,7 +13,11 @@
// limitations under the License.

import * as vscode from "vscode";
import { queryQuickPickTargets } from "../bazel";
import {
BazelWorkspaceInfo,
getPackageLabelForBuildFile,
queryQuickPickTargets,
} from "../bazel";

function insertCompletionItemIfUnique(
options: vscode.CompletionItem[],
@@ -35,7 +39,15 @@ function getCandidateTargetFromDocumentPosition(
const linePrefix = document
.lineAt(position)
.text.substring(0, position.character);
const index = linePrefix.lastIndexOf("//");
const atIndex = linePrefix.lastIndexOf("@");
const doubleSlashIndex = linePrefix.lastIndexOf("//");
const colonIndex = linePrefix.lastIndexOf(":");
const index =
atIndex !== -1
? atIndex
: doubleSlashIndex !== -1
? doubleSlashIndex
: colonIndex;
if (index === -1) {
return undefined;
}
@@ -65,16 +77,37 @@ function getNextPackage(target: string) {
return undefined;
}

export class BazelCompletionItemProvider
function getAbsoluteLabel(
target: string,
document: vscode.TextDocument,
): string {
if (target.startsWith("//") || target.startsWith("@")) {
return target;
}
const workspace = BazelWorkspaceInfo.fromDocument(document);
if (!workspace) {
return target;
}
const packageLabel = getPackageLabelForBuildFile(
workspace.bazelWorkspacePath,
document.uri.fsPath,
);
return `${packageLabel}${target}`;
}

function getRepositoryName(target: string): string {
const endOfRepo = target.indexOf("//");
return endOfRepo <= 0 ? "" : target.substring(1, endOfRepo);
}

export class BazelTargetCompletionItemProvider
implements vscode.CompletionItemProvider {
private targets: string[] = [];
private readonly targetsInRepo = new Map<string, Promise<string[]>>();

/**
* Returns completion items matching the given prefix.
*
* Only label started with "//: is supported at the moment.
*/
public provideCompletionItems(
public async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
) {
@@ -86,22 +119,35 @@ export class BazelCompletionItemProvider
return [];
}

candidateTarget = getAbsoluteLabel(candidateTarget, document);
if (!candidateTarget.endsWith("/") && !candidateTarget.endsWith(":")) {
candidateTarget = stripLastPackageOrTargetName(candidateTarget);
}

const repo = getRepositoryName(candidateTarget);
if (repo !== "") {
const bazelConfig = vscode.workspace.getConfiguration("bazel");
const enableExternalTargetCompletion = bazelConfig.get<boolean>(
"enableExternalTargetCompletion",
);
if (!enableExternalTargetCompletion) {
return [];
}
}

const targets = await this.getTargetsDefinedInRepo(repo);
const completionItems = new Array<vscode.CompletionItem>();
this.targets.forEach((target) => {
targets.forEach((target) => {
if (!target.startsWith(candidateTarget)) {
return;
}
const sufix = target.replace(candidateTarget, "");
const suffix = target.replace(candidateTarget, "");

let completionKind = vscode.CompletionItemKind.Folder;
let label = getNextPackage(sufix);
let label = getNextPackage(suffix);
if (label === undefined) {
completionKind = vscode.CompletionItemKind.Field;
label = sufix;
label = suffix;
}
insertCompletionItemIfUnique(
completionItems,
@@ -115,12 +161,27 @@ export class BazelCompletionItemProvider
* Runs a bazel query command to acquire labels of all the targets in the
* workspace.
*/
public async refresh() {
const queryTargets = await queryQuickPickTargets("kind('.* rule', ...)");
if (queryTargets.length !== 0) {
this.targets = queryTargets.map((queryTarget) => {
return queryTarget.label;
});
public async refresh(): Promise<void> {
this.targetsInRepo.clear();
await this.queryAndCacheTargets();
}

private async getTargetsDefinedInRepo(repository = ""): Promise<string[]> {
const deferred = this.targetsInRepo.get(repository);
if (deferred) {
return await deferred;
}
return await this.queryAndCacheTargets(repository);
}

private async queryAndCacheTargets(repository = ""): Promise<string[]> {
const queryTargets = async () => {
const query = `kind('.* rule', @${repository}//...)`;
const targets = await queryQuickPickTargets(query);
return targets.map((target) => target.label);
};
const deferred = queryTargets();
this.targetsInRepo.set(repository, deferred);
return await deferred;
}
}
3 changes: 2 additions & 1 deletion src/completion-provider/index.ts
Original file line number Diff line number Diff line change
@@ -12,4 +12,5 @@
// See the License for the specific language governing permissions and
// limitations under the License.

export * from "./bazel_completion_provider";
export * from "./bazel_repository_completion_provider";
export * from "./bazel_target_completion_provider";
24 changes: 19 additions & 5 deletions src/extension/extension.ts
Original file line number Diff line number Diff line change
@@ -35,7 +35,10 @@ import {
checkBuildifierIsAvailable,
} from "../buildifier";
import { BazelBuildCodeLensProvider } from "../codelens";
import { BazelCompletionItemProvider } from "../completion-provider";
import {
BazelRepositoryCompletionItemProvider,
BazelTargetCompletionItemProvider,
} from "../completion-provider";
import { BazelGotoDefinitionProvider } from "../definition/bazel_goto_definition_provider";
import { BazelTargetSymbolProvider } from "../symbols";
import { BazelWorkspaceTreeProvider } from "../workspace-tree";
@@ -51,15 +54,24 @@ export function activate(context: vscode.ExtensionContext) {
const workspaceTreeProvider = new BazelWorkspaceTreeProvider(context);
const codeLensProvider = new BazelBuildCodeLensProvider(context);
const buildifierDiagnostics = new BuildifierDiagnosticsManager();
const completionItemProvider = new BazelCompletionItemProvider();
const repositoryCompletionItemProvider =
new BazelRepositoryCompletionItemProvider();
const targetCompletionItemProvider = new BazelTargetCompletionItemProvider();

// tslint:disable-next-line:no-floating-promises
completionItemProvider.refresh();
repositoryCompletionItemProvider.refresh();
// tslint:disable-next-line:no-floating-promises
targetCompletionItemProvider.refresh();

context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
[{ pattern: "**/BUILD" }, { pattern: "**/BUILD.bazel" }],
completionItemProvider,
repositoryCompletionItemProvider,
"@",
),
vscode.languages.registerCompletionItemProvider(
[{ pattern: "**/BUILD" }, { pattern: "**/BUILD.bazel" }],
targetCompletionItemProvider,
"/",
":",
),
@@ -88,7 +100,9 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("bazel.clean", bazelClean),
vscode.commands.registerCommand("bazel.refreshBazelBuildTargets", () => {
// tslint:disable-next-line:no-floating-promises
completionItemProvider.refresh();
repositoryCompletionItemProvider.refresh();
// tslint:disable-next-line:no-floating-promises
targetCompletionItemProvider.refresh();
workspaceTreeProvider.refresh();
}),
vscode.commands.registerCommand(