diff --git a/src/codelens/bazel_build_code_lens_provider.ts b/src/codelens/bazel_build_code_lens_provider.ts index fa5e4145..f5e850a7 100644 --- a/src/codelens/bazel_build_code_lens_provider.ts +++ b/src/codelens/bazel_build_code_lens_provider.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import * as path from "path"; import * as vscode from "vscode"; import { BazelWorkspaceInfo, QueryLocation } from "../bazel"; @@ -20,6 +21,48 @@ import { getDefaultBazelExecutablePath } from "../extension/configuration"; import { blaze_query } from "../protos"; import { CodeLensCommandAdapter } from "./code_lens_command_adapter"; +/** Returns a target string without some prefix. + * + * If the target string does not have the prefix, returns the string unchanged. + * + * @param target The string whose prefix to remove. + * @param prefix The prefix to remove, if present. + * @returns The target string without the provided prefix. + */ +function withoutPrefix(target: string, prefix: string): string { + if (!target.startsWith(prefix)) { + return target; + } + return target.substring(prefix.length); +} + +/** Computes the shortened name of a Bazel target. + * + * For example, if the target name starts with `//foo/bar/baz:fizbuzz` and we're + * in `foo/bar/baz/BUILD.bazel`, the target's short name will be `fizzbuzz`. + * + * This allows our code lens suggestions to avoid filling users' screen with + * redundant path information. + * + * @param targetName The unshortened name of the target. + * @param bazelWorkspaceInfo The currrent Bazel workspace. + * @param location The location of the target's BUILD.bazel file. + * + * @returns The shortened name of the target. + */ +function getTargetShortName( + targetName: string, + bazelWorkspaceInfo: BazelWorkspaceInfo, + location: QueryLocation, +): string { + const pathInWorkspace = path.dirname( + path.relative(bazelWorkspaceInfo.bazelWorkspacePath, location.path), + ); + // On Windows, path separators are different from Bazel's `/` separator. + const bazelStylePathInWorkspace = pathInWorkspace.replaceAll(path.sep, "/"); + return withoutPrefix(targetName, "//" + bazelStylePathInWorkspace + ":"); +} + /** Provids CodeLenses for targets in Bazel BUILD files. */ export class BazelBuildCodeLensProvider implements vscode.CodeLensProvider { public onDidChangeCodeLenses: vscode.Event; @@ -107,40 +150,62 @@ export class BazelBuildCodeLensProvider implements vscode.CodeLensProvider { ): vscode.CodeLens[] { const result: vscode.CodeLens[] = []; + interface LensCommand { + commandString: string; + name: string; + } + for (const target of queryResult.target) { const location = new QueryLocation(target.rule.location); const targetName = target.rule.name; const ruleClass = target.rule.ruleClass; - let cmd: vscode.Command; + let targetShortName = getTargetShortName( + targetName, + bazelWorkspaceInfo, + location, + ); + + const commands: LensCommand[] = []; + + // Only test targets support testing. if (ruleClass.endsWith("_test") || ruleClass === "test_suite") { - cmd = { - arguments: [ - new CodeLensCommandAdapter(bazelWorkspaceInfo, [targetName]), - ], - command: "bazel.testTarget", - title: `Test ${targetName}`, - tooltip: `Test ${targetName}`, - }; - } else if (ruleClass.endsWith("_binary")) { - cmd = { - arguments: [ - new CodeLensCommandAdapter(bazelWorkspaceInfo, [targetName]), - ], - command: "bazel.runTarget", - title: `Run ${targetName}`, - tooltip: `Run ${targetName}`, - }; - } else { - cmd = { - arguments: [ - new CodeLensCommandAdapter(bazelWorkspaceInfo, [targetName]), - ], - command: "bazel.buildTarget", - title: `Build ${targetName}`, - tooltip: `Build ${targetName}`, - }; + commands.push({ + commandString: "bazel.testTarget", + name: "Test", + }); + } + + // Targets which are not libraries may support running. + // + // Without checking the Bazel rule's `executable` attribute we can't know + // for sure which targets can be run. + const ruleIsLibrary = ruleClass.endsWith("_library"); + if (!ruleIsLibrary) { + commands.push({ + commandString: "bazel.runTarget", + name: "Run", + }); + } + + // All targets support building. + commands.push({ + commandString: "bazel.buildTarget", + name: "Build", + }); + + for (const command of commands) { + const title = `${command.name} ${targetShortName}`; + result.push( + new vscode.CodeLens(location.range, { + arguments: [ + new CodeLensCommandAdapter(bazelWorkspaceInfo, [targetName]), + ], + command: command.commandString, + title, + tooltip: title, + }), + ); } - result.push(new vscode.CodeLens(location.range, cmd)); } return result;