Skip to content
Open
Show file tree
Hide file tree
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
61 changes: 61 additions & 0 deletions src/bazel/bazel_quickpick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,64 @@ export async function queryQuickPickPackage({
.sort()
.map((target) => new BazelTargetQuickPick("//" + target, workspaceInfo));
}

/** Maximum length for target display names before truncation */
const MAX_TARGET_DISPLAY_LENGTH = 80;

/**
* Creates a formatted display name for a target with proper truncation
*/
function formatTargetDisplayName(
target: string,
maxLabelLength: number = MAX_TARGET_DISPLAY_LENGTH,
): string {
const shortName = target.includes(":") ? target.split(":")[1] : target;
// Truncate from the beginning if the name is too long (keep the end visible)
return shortName.length > maxLabelLength
? "..." + shortName.slice(-(maxLabelLength - 3))
: shortName;
}

/**
* Creates QuickPick items for targets with consistent formatting
*/
export function createTargetQuickPickItems(targets: string[]): {
label: string;
description: string;
target: string;
}[] {
return targets.map((target) => ({
label: formatTargetDisplayName(target),
description: target, // Full target path as description
target,
}));
}

/**
* Shows a QuickPick for multiple targets and returns the selected target
* @param targets Array of target strings to choose from
* @param commandName Name of the command for display purposes
* @returns Promise that resolves to the selected target string, or undefined if cancelled
*/
export async function showTargetQuickPick(
targets: string[],
commandName: string,
): Promise<string | undefined> {
if (targets.length === 0) {
return undefined;
}

if (targets.length === 1) {
return targets[0];
}

// Show QuickPick for multiple targets
const quickPickItems = createTargetQuickPickItems(targets);

const selectedItem = await vscode.window.showQuickPick(quickPickItems, {
placeHolder: `Select target to ${commandName.toLowerCase()}`,
canPickMany: false,
});

return selectedItem?.target;
}
220 changes: 150 additions & 70 deletions src/codelens/bazel_build_code_lens_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,24 @@ import { getDefaultBazelExecutablePath } from "../extension/configuration";
import { blaze_query } from "../protos";
import { CodeLensCommandAdapter } from "./code_lens_command_adapter";

/** Computes the shortened name of a Bazel target.
/**
* Groups of Bazel targets organized by the actions they support.
* Used by the CodeLens provider to determine which actions to display for each target.
*
* For example, if the target name starts with `//foo/bar/baz:fizbuzz`,
* 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.
* @returns The shortened name of the target.
* @interface ActionGroups
* @property {string[]} copy - Targets that support copying their label to clipboard (all target types)
* @property {string[]} build - Targets that support build operations (libraries, binaries, tests)
* @property {string[]} test - Targets that support test execution (test rules only)
* @property {string[]} run - Targets that support run operations (executable binaries only)
*/
function getTargetShortName(targetName: string): string {
const colonFragments = targetName.split(":");
if (colonFragments.length !== 2) {
return targetName;
}
return colonFragments[1];
interface ActionGroups {
copy: string[];
build: string[];
test: string[];
run: string[];
}

/** Provids CodeLenses for targets in Bazel BUILD files. */
/** Provides CodeLenses for targets in Bazel BUILD files. */
export class BazelBuildCodeLensProvider implements vscode.CodeLensProvider {
public onDidChangeCodeLenses: vscode.Event<void>;

Expand Down Expand Up @@ -117,59 +115,118 @@ export class BazelBuildCodeLensProvider implements vscode.CodeLensProvider {
* Takes the result of a Bazel query for targets defined in a package and
* returns a list of CodeLens for the BUILD file in that package.
*
* @param bazelWorkspaceDirectory The Bazel workspace directory.
* @param queryResult The result of the bazel query.
* @param bazelWorkspaceInfo The Bazel workspace information containing workspace path and context
* @param queryResult The result of the bazel query containing target definitions
* @returns A new array of CodeLens objects for the BUILD file
*/
private computeCodeLenses(
bazelWorkspaceInfo: BazelWorkspaceInfo,
queryResult: blaze_query.QueryResult,
): vscode.CodeLens[] {
const result: vscode.CodeLens[] = [];

interface LensCommand {
commandString: string;
name: string;
}

const useTargetMap = queryResult.target
.map((t) => new QueryLocation(t.rule.location).line)
.reduce((countMap, line) => {
countMap.set(line, countMap.has(line));
return countMap;
}, new Map<number, boolean>());
// Sort targets by length first, then alphabetically
// This ensures shorter names (often main targets) appear first, with consistent ordering within each length group
// Sort targets alphabetically
const sortedTargets = [...queryResult.target].sort((a, b) => {
const lengthDiff = a.rule.name.length - b.rule.name.length;
return lengthDiff !== 0
? lengthDiff
: a.rule.name.localeCompare(b.rule.name);
return a.rule.name.localeCompare(b.rule.name);
});

// Group targets by line number to handle multiple targets on same line
const targetsByLine = new Map<number, typeof sortedTargets>();
for (const target of sortedTargets) {
const location = new QueryLocation(target.rule.location);
const line = location.line;
if (!targetsByLine.has(line)) {
targetsByLine.set(line, []);
}
targetsByLine.get(line)?.push(target);
}

// Process each line's targets
for (const [, targets] of targetsByLine) {
this.createCodeLensesForTargetsOnSameLine(
targets,
bazelWorkspaceInfo,
result,
);
}

return result;
}

/**
* Creates CodeLens objects for targets on the same line.
*
* @param targets Array of Bazel targets found on the same line in the BUILD file
* @param bazelWorkspaceInfo Workspace context information for command creation
* @param result Output array that will be modified in-place to include new CodeLens objects
*/
private createCodeLensesForTargetsOnSameLine(
targets: blaze_query.ITarget[],
bazelWorkspaceInfo: BazelWorkspaceInfo,
result: vscode.CodeLens[],
): void {
const location = new QueryLocation(targets[0].rule.location);

const actionGroups = this.groupTargetsByAction(targets);

this.createCodeLens(
"Copy",
"bazel.copyLabelToClipboard",
actionGroups.copy,
location,
bazelWorkspaceInfo,
result,
);
this.createCodeLens(
"Build",
"bazel.buildTarget",
actionGroups.build,
location,
bazelWorkspaceInfo,
result,
);
this.createCodeLens(
"Test",
"bazel.testTarget",
actionGroups.test,
location,
bazelWorkspaceInfo,
result,
);
this.createCodeLens(
"Run",
"bazel.runTarget",
actionGroups.run,
location,
bazelWorkspaceInfo,
result,
);
}

/**
* Groups targets by the actions they support based on Bazel rule types.
* Uses rule naming conventions to determine which actions are available.
*
* @param targets Array of Bazel targets to classify by supported actions
* @returns ActionGroups object with targets organized by action type
*/
private groupTargetsByAction(targets: blaze_query.ITarget[]): ActionGroups {
const copyTargets: string[] = [];
const buildTargets: string[] = [];
const testTargets: string[] = [];
const runTargets: string[] = [];

for (const target of targets) {
const targetName = target.rule.name;
const ruleClass = target.rule.ruleClass;
const targetShortName = getTargetShortName(targetName);

const commands: LensCommand[] = [];

// All targets support target copying and building.
commands.push({
commandString: "bazel.copyLabelToClipboard",
name: "Copy",
});
commands.push({
commandString: "bazel.buildTarget",
name: "Build",
});
// All targets support copying and building
copyTargets.push(targetName);
buildTargets.push(targetName);

// Only test targets support testing.
if (ruleClass.endsWith("_test") || ruleClass === "test_suite") {
commands.push({
commandString: "bazel.testTarget",
name: "Test",
});
testTargets.push(targetName);
}

// Targets which are not libraries may support running.
Expand All @@ -180,28 +237,51 @@ export class BazelBuildCodeLensProvider implements vscode.CodeLensProvider {
// first running the `analysis` phase, so we use a heuristic instead.
const ruleIsLibrary = ruleClass.endsWith("_library");
if (!ruleIsLibrary) {
commands.push({
commandString: "bazel.runTarget",
name: "Run",
});
runTargets.push(targetName);
}
}

for (const command of commands) {
const tooltip = `${command.name} ${targetShortName}`;
const title = useTargetMap.get(location.line) ? tooltip : command.name;
result.push(
new vscode.CodeLens(location.range, {
arguments: [
new CodeLensCommandAdapter(bazelWorkspaceInfo, [targetName]),
],
command: command.commandString,
title,
tooltip,
}),
);
}
return {
copy: copyTargets,
build: buildTargets,
test: testTargets,
run: runTargets,
};
}

/**
* Creates a CodeLens for a specific action type if targets are available.
* Title shows action name with count for multiple targets.
*
* @param actionName Display name for the action (e.g., "Build", "Test", "Run", "Copy")
* @param command VS Code command identifier to execute when CodeLens is clicked
* @param targets Array of target names that support this action
* @param location Source location information for CodeLens positioning
* @param bazelWorkspaceInfo Workspace context for command adapter creation
* @param result Output array that will be modified in-place to include the new CodeLens
*/
private createCodeLens(
actionName: string,
command: string,
targets: string[],
location: QueryLocation,
bazelWorkspaceInfo: BazelWorkspaceInfo,
result: vscode.CodeLens[],
): void {
if (targets.length === 0) {
return;
}

return result;
const title =
targets.length === 1 ? actionName : `${actionName} (${targets.length})`;

result.push(
new vscode.CodeLens(location.range, {
arguments: [new CodeLensCommandAdapter(bazelWorkspaceInfo, targets)],
command,
title,
tooltip: `${actionName} target - ${targets.length} targets available`,
}),
);
}
}
Loading