Skip to content

Commit

Permalink
feat: Introduce pickTarget and pickPackage command variables (#354)
Browse files Browse the repository at this point in the history
This commit introduces the `bazel.pickTarget` and `bazel.pickPackage`
command variables. Those commands can be used from `tasks.json` and
`launch.json` to prompt the user to select a Bazel target or package.
The available choices are determined by a user-specified Bazel query.

The implementation reuses the existing quick-pick functionality from
`bazel_quickpick.ts`. The `wrapQuickPick` function parses the arguments
passed from the `task.json` and then forwards to the existing quickpicks.

Those utility functions were restructured as part of this commit:
1. The internal helpers `queryWorkspaceQuickPickPackages` and
   `queryWorkspaceQuickPickTargets` were inlined into
   `queryQuickPick{Packages,Targets}` as the internal helper functions
   got in the way of other changes, but didn't deduplicate any code as
   they only had one caller.
2. The `queryQuickPick{Packages,Targets}` take an object with named
   parameters now.
3. `queryQuickPickPackage` now also accepts a `query` parameter instead
   of using the `queryExpression`. Both `queryQuickPickPackages` and
   `queryQuickPickTargets` default to the query `//...` if no query
   string is provided by the caller.

Point (3) also is a user-facing change. Commands like "Build package" and
"Test package" now use `//...` instead of the configured `queryExpression`.
This is in sync with other commands like "Test target" which uses the
hardcoded query `kind('.*_test rule', ...)`.
  • Loading branch information
vogelsgesang committed Apr 2, 2024
1 parent e50cb97 commit 2ef6d2a
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 92 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ This extension can use [Facebook's starlark project](https://github.com/facebook

## Bazel tasks

Bazel tasks can be configured from the `launch.json` using the following structure:
Bazel tasks can be configured from the `tasks.json` using the following structure:

```json
{
Expand All @@ -71,9 +71,20 @@ Bazel tasks can be configured from the `launch.json` using the following structu
"label": "Check for flakyness",
"type": "bazel",
"command": "test",
"targets": ["//my/package:integration_test"],
"targets": ["${input:pickFlakyTest}"],
"options": ["--runs_per_test=9"]
}
],
"inputs": [
{
"id": "pickFlakyTest",
"type": "command",
"command": "bazel.pickTarget",
"args": {
"query": "kind('.*_test', //...:*)",
"placeHolder": "Which test to check for flakyness?"
}
}
]
}
```
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"workspaceContains:**/WORKSPACE.bazel",
"workspaceContains:**/MODULE.bazel",
"workspaceContains:**/REPO.bazel",
"onCommand:bazel.pickPackage",
"onCommand:bazel.pickTarget",
"onCommand:bazel.getTargetOutput",
"onCommand:bazel.info.bazel-bin",
"onCommand:bazel.info.bazel-genfiles",
Expand Down
27 changes: 27 additions & 0 deletions src/assert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as vscode from "vscode";

let assertionFailureReported = false;

/**
* Asserts that the given value is true.
*/
export function assert(value: boolean): asserts value {
if (!value) {
debugger; // eslint-disable-line no-debugger
if (!assertionFailureReported) {
// Only report one assertion failure, to avoid spamming the
// user with error messages.
assertionFailureReported = true;
// Log an `Error` object which will include the stack trace
// eslint-disable-next-line no-console
console.error(new Error("Assertion violated."));
// eslint-disable-next-line @typescript-eslint/no-floating-promises
vscode.window.showErrorMessage(
"Assertion violated. This is a programming error.\n" +
"Please file a bug at " +
"https://github.com/bazelbuild/vscode-bazel/issues",
);
}
throw new Error("Assertion violated.");
}
}
140 changes: 58 additions & 82 deletions src/bazel/bazel_quickpick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,114 +66,90 @@ export class BazelTargetQuickPick
}

/**
* Runs the given bazel query command in the given bazel workspace and returns
* the resulting array of BazelTargetQuickPick as a promise.
* @param workspace The bazel workspace to run the bazel command from.
* @param query The bazel query string to run.
* Use the active text editor's file to determine the directory of the Bazel
* workspace, otherwise have them pick one.
*/
async function queryWorkspaceQuickPickTargets(
workspaceInfo: BazelWorkspaceInfo,
query: string,
): Promise<BazelTargetQuickPick[]> {
const queryResult = await new BazelQuery(
getDefaultBazelExecutablePath(),
workspaceInfo.workspaceFolder.uri.fsPath,
).queryTargets(query);
// Sort the labels so the QuickPick is ordered.
const labels = queryResult.target.map((target) => target.rule.name);
labels.sort();
const result: BazelTargetQuickPick[] = [];
for (const target of labels) {
result.push(new BazelTargetQuickPick(target, workspaceInfo));
}
return result;
}

/**
* Runs a bazel query command for pacakges in the given bazel workspace and
* returns the resulting array of BazelTargetQuickPick as a promise.
* @param workspace The bazel workspace to run the bazel command from.
*/
async function queryWorkspaceQuickPickPackages(
workspaceInfo: BazelWorkspaceInfo,
): Promise<BazelTargetQuickPick[]> {
const packagePaths = await new BazelQuery(
getDefaultBazelExecutablePath(),
workspaceInfo.workspaceFolder.uri.fsPath,
).queryPackages(
vscode.workspace
.getConfiguration("bazel.commandLine")
.get("queryExpression"),
);
const result: BazelTargetQuickPick[] = [];
for (const target of packagePaths) {
result.push(new BazelTargetQuickPick("//" + target, workspaceInfo));
}
return result;
}

async function pickBazelWorkspace(): Promise<BazelWorkspaceInfo> {
// Use the active text editor's file to determine the directory of the Bazel
// workspace, otherwise have them pick one.
let workspace: BazelWorkspaceInfo;
async function pickBazelWorkspace(): Promise<BazelWorkspaceInfo | undefined> {
if (vscode.window.activeTextEditor === undefined) {
let workspaceFolder: vscode.WorkspaceFolder;
const workspaces = vscode.workspace.workspaceFolders;
switch (workspaces.length) {
case 0:
workspaceFolder = undefined;
break;
case 1:
workspaceFolder = workspaces[0];
break;
default:
workspaceFolder = await vscode.window.showWorkspaceFolderPick();
break;
}
workspace = BazelWorkspaceInfo.fromWorkspaceFolder(workspaceFolder);
return BazelWorkspaceInfo.fromWorkspaceFolders();
} else {
const document = vscode.window.activeTextEditor.document;
workspace = BazelWorkspaceInfo.fromDocument(document);
return BazelWorkspaceInfo.fromDocument(document);
}
}

return workspace;
export interface QuickPickParams {
// The bazel query string to run.
query?: string;
// The bazel workspace to run the bazel command from.
workspaceInfo?: BazelWorkspaceInfo;
}

/**
* Runs the given bazel query command in an automatically determined bazel
* workspace and returns the resulting array of BazelTargetQuickPick as a
* promise. The workspace is determined by trying to determine the bazel
* Runs the given bazel query command in the given bazel workspace and returns
* the resulting array of BazelTargetQuickPick.
*
* If no workspace is given, uses an automatically determined bazel
* workspace. The workspace is determined by trying to determine the bazel
* workspace the currently active text editor is in.
* @param query The bazel query string to run.
*/
export async function queryQuickPickTargets(
query: string,
): Promise<BazelTargetQuickPick[]> {
const workspace: BazelWorkspaceInfo = await pickBazelWorkspace();
export async function queryQuickPickTargets({
query,
workspaceInfo,
}: QuickPickParams): Promise<BazelTargetQuickPick[]> {
if (workspaceInfo === undefined) {
// Ask the user to pick a workspace, if we don't have one, yet
workspaceInfo = await pickBazelWorkspace();
}

if (workspace === undefined) {
if (workspaceInfo === undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
vscode.window.showErrorMessage("Failed to find a Bazel workspace");
return [];
}

return queryWorkspaceQuickPickTargets(workspace, query);
const queryResult = await new BazelQuery(
getDefaultBazelExecutablePath(),
workspaceInfo.workspaceFolder.uri.fsPath,
).queryTargets(query ?? "//...:*");

// Sort the labels so the QuickPick is ordered.
const labels = queryResult.target.map((target) => target.rule.name);
labels.sort();
return labels.map(
(target) => new BazelTargetQuickPick(target, workspaceInfo),
);
}

/**
* Runs a bazel query command for package in an automatically determined bazel
* workspace and returns the resulting array of BazelTargetQuickPick as a
* promise. The workspace is determined by trying to determine the bazel
* Runs the given bazel query command in the given bazel workspace and returns
* the resulting array of BazelTargetQuickPick.
*
* If no workspace is given, uses an automatically determined bazel
* workspace. The workspace is determined by trying to determine the bazel
* workspace the currently active text editor is in.
*/
export async function queryQuickPickPackage(): Promise<BazelTargetQuickPick[]> {
const workspace: BazelWorkspaceInfo = await pickBazelWorkspace();
export async function queryQuickPickPackage({
query,
workspaceInfo,
}: QuickPickParams): Promise<BazelTargetQuickPick[]> {
if (workspaceInfo === undefined) {
// Ask the user to pick a workspace, if we don't have one, yet
workspaceInfo = await pickBazelWorkspace();
}

if (workspace === undefined) {
if (workspaceInfo === undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
vscode.window.showErrorMessage("Failed to find a Bazel workspace");
return [];
}

return queryWorkspaceQuickPickPackages(workspace);
const packagePaths = await new BazelQuery(
getDefaultBazelExecutablePath(),
workspaceInfo.workspaceFolder.uri.fsPath,
).queryPackages(query ?? "//...");

return packagePaths.map(
(target) => new BazelTargetQuickPick("//" + target, workspaceInfo),
);
}
4 changes: 3 additions & 1 deletion src/completion-provider/bazel_completion_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ export class BazelCompletionItemProvider
* workspace.
*/
public async refresh() {
const queryTargets = await queryQuickPickTargets("kind('.* rule', ...)");
const queryTargets = await queryQuickPickTargets({
query: "kind('.* rule', ...)",
});
if (queryTargets.length !== 0) {
this.targets = queryTargets.map((queryTarget) => {
return queryTarget.label;
Expand Down
90 changes: 89 additions & 1 deletion src/extension/command_variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@
import * as path from "path";
import * as vscode from "vscode";
import { getDefaultBazelExecutablePath } from "./configuration";
import { BazelWorkspaceInfo } from "../bazel";
import {
BazelTargetQuickPick,
BazelWorkspaceInfo,
QuickPickParams,
queryQuickPickPackage,
queryQuickPickTargets,
} from "../bazel";
import { BazelCQuery } from "../bazel/bazel_cquery";
import { BazelInfo } from "../bazel/bazel_info";
import { assert } from "console";

/**
* Get the output of the given target.
Expand Down Expand Up @@ -118,6 +125,79 @@ async function bazelInfo(key: string): Promise<string> {
).run(key);
}

/**
* Gets a string-valued argument in a typesafe manner from an object.
* Throws `Error`s with user-friendly error messages in case of an error.
*
* @param args the arguments
* @param argName the argument name
* @param commandName the commmand name. Used in the error message
* @returns the extracted string value
*/
function getArgumentValue(
args: Record<string, any>,
argName: string,
commandName: string,
): string | undefined {
if (argName in args && typeof args[argName] === "string") {
return args[argName] as string;
} else if (argName in args) {
throw new Error(
`Expected the \`${argName}\` argument for \`${commandName}\` to be a string`,
);
}
}

/**
* Wraps the `queryQuickPickPackage` / `queryQuickPickTargets` functions
* so they can be exposed as command variables.
*/
async function wrapQuickPick(
commandName: string,
queryQuickPick: (x: QuickPickParams) => Promise<BazelTargetQuickPick[]>,
args: unknown,
): Promise<string | undefined> {
const workspaceInfo = await BazelWorkspaceInfo.fromWorkspaceFolders();
if (!workspaceInfo) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
vscode.window.showInformationMessage(
"Please open a Bazel workspace folder to use this command.",
);

return;
}

// Default values, overridable from the `tasks.json` invocation
let query = "//...";
let placeHolder = "";

// Interpret the arguments
if (args) {
if (!(args instanceof Object) || args instanceof Array) {
throw new Error(
`Expected the \`args\` for \`${commandName}\` to be an object`,
);
} else {
query = getArgumentValue(args, "query", commandName) ?? query;
placeHolder =
getArgumentValue(args, "placeHolder", commandName) ?? placeHolder;
}
}
const quickPick = await vscode.window.showQuickPick(
queryQuickPick({ query, workspaceInfo }),
{
canPickMany: false,
placeHolder,
},
);
if (quickPick === undefined) {
// If the user cancelled the quick pick, fail the substitution
return;
}
assert(quickPick.getBazelCommandOptions().targets.length === 1);
return quickPick.getBazelCommandOptions().targets[0];
}

/**
* Activate all "command variables"
*/
Expand All @@ -127,6 +207,14 @@ export function activateCommandVariables(): vscode.Disposable[] {
"bazel.getTargetOutput",
bazelGetTargetOutput,
),
...["pickPackage", "pickTarget"].map((key, idx) => {
const commandName = `bazel.${key}`;
const funcs = [queryQuickPickPackage, queryQuickPickTargets];
const func = funcs[idx];
return vscode.commands.registerCommand(commandName, (args) =>
wrapQuickPick(commandName, func, args),
);
}),
...[
"bazel-bin",
"bazel-genfiles",
Expand Down

0 comments on commit 2ef6d2a

Please sign in to comment.