diff --git a/package.json b/package.json index 9f08ad67..7927123a 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/bazel/bazel_utils.ts b/src/bazel/bazel_utils.ts index 773cbd7b..2f7ab4c0 100644 --- a/src/bazel/bazel_utils.ts +++ b/src/bazel/bazel_utils.ts @@ -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 { +): 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 { + const pkg = getPackageLabelForBuildFile(workspace, buildFile); const queryResult = await new BazelQuery( bazelExecutable, workspace, diff --git a/src/completion-provider/bazel_repository_completion_provider.ts b/src/completion-provider/bazel_repository_completion_provider.ts new file mode 100644 index 00000000..546ac26d --- /dev/null +++ b/src/completion-provider/bazel_repository_completion_provider.ts @@ -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; + + /** + * 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( + "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 { + await this.queryAndCacheRepos(); + } + + private async getRepos(): Promise { + if (this.repositories) { + return await this.repositories; + } + return await this.queryAndCacheRepos(); + } + + private async queryAndCacheRepos(): Promise { + 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; + } +} diff --git a/src/completion-provider/bazel_completion_provider.ts b/src/completion-provider/bazel_target_completion_provider.ts similarity index 54% rename from src/completion-provider/bazel_completion_provider.ts rename to src/completion-provider/bazel_target_completion_provider.ts index d9f64631..a0886c24 100644 --- a/src/completion-provider/bazel_completion_provider.ts +++ b/src/completion-provider/bazel_target_completion_provider.ts @@ -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>(); /** * 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( + "enableExternalTargetCompletion", + ); + if (!enableExternalTargetCompletion) { + return []; + } + } + + const targets = await this.getTargetsDefinedInRepo(repo); const completionItems = new Array(); - 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 { + this.targetsInRepo.clear(); + await this.queryAndCacheTargets(); + } + + private async getTargetsDefinedInRepo(repository = ""): Promise { + const deferred = this.targetsInRepo.get(repository); + if (deferred) { + return await deferred; } + return await this.queryAndCacheTargets(repository); + } + + private async queryAndCacheTargets(repository = ""): Promise { + 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; } } diff --git a/src/completion-provider/index.ts b/src/completion-provider/index.ts index c5c213a2..792b30e0 100644 --- a/src/completion-provider/index.ts +++ b/src/completion-provider/index.ts @@ -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"; diff --git a/src/extension/extension.ts b/src/extension/extension.ts index d4a6c566..ca82758d 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -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(