diff --git a/README.md b/README.md index df3827e4..fad9a355 100644 --- a/README.md +++ b/README.md @@ -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 { @@ -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?" + } + } ] } ``` diff --git a/package.json b/package.json index 546bf4c8..b297d9cd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/assert.ts b/src/assert.ts new file mode 100644 index 00000000..feb658a9 --- /dev/null +++ b/src/assert.ts @@ -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."); + } +} diff --git a/src/bazel/bazel_quickpick.ts b/src/bazel/bazel_quickpick.ts index 2ca265d6..8d508c37 100644 --- a/src/bazel/bazel_quickpick.ts +++ b/src/bazel/bazel_quickpick.ts @@ -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 { - 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 { - 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 { - // 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 { 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 { - const workspace: BazelWorkspaceInfo = await pickBazelWorkspace(); +export async function queryQuickPickTargets({ + query, + workspaceInfo, +}: QuickPickParams): Promise { + 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 { - const workspace: BazelWorkspaceInfo = await pickBazelWorkspace(); +export async function queryQuickPickPackage({ + query, + workspaceInfo, +}: QuickPickParams): Promise { + 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), + ); } diff --git a/src/completion-provider/bazel_completion_provider.ts b/src/completion-provider/bazel_completion_provider.ts index c0231d87..c0efc061 100644 --- a/src/completion-provider/bazel_completion_provider.ts +++ b/src/completion-provider/bazel_completion_provider.ts @@ -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; diff --git a/src/extension/command_variables.ts b/src/extension/command_variables.ts index bc9b8d04..b7ad91be 100644 --- a/src/extension/command_variables.ts +++ b/src/extension/command_variables.ts @@ -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. @@ -118,6 +125,79 @@ async function bazelInfo(key: string): Promise { ).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, + 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, + args: unknown, +): Promise { + 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" */ @@ -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", diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 3ca0dcdc..280b230e 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -189,7 +189,7 @@ async function bazelBuildTarget(adapter: IBazelCommandAdapter | undefined) { // invoked via the command palatte. Provide quickpick build targets for // the user to choose from. const quickPick = await vscode.window.showQuickPick( - queryQuickPickTargets("kind('.* rule', ...)"), + queryQuickPickTargets({ query: "kind('.* rule', ...)" }), { canPickMany: false, }, @@ -221,7 +221,7 @@ async function bazelBuildTargetWithDebugging( // invoked via the command palatte. Provide quickpick build targets for // the user to choose from. const quickPick = await vscode.window.showQuickPick( - queryQuickPickTargets("kind('.* rule', ...)"), + queryQuickPickTargets({ query: "kind('.* rule', ...)" }), { canPickMany: false, }, @@ -289,7 +289,7 @@ async function buildPackage( // invoked via the command palatte. Provide quickpick build targets for // the user to choose from. const quickPick = await vscode.window.showQuickPick( - queryQuickPickPackage(), + queryQuickPickPackage({}), { canPickMany: false, }, @@ -324,7 +324,7 @@ async function bazelRunTarget(adapter: IBazelCommandAdapter | undefined) { // invoked via the command palatte. Provide quickpick test targets for // the user to choose from. const quickPick = await vscode.window.showQuickPick( - queryQuickPickTargets("kind('.* rule', ...)"), + queryQuickPickTargets({ query: "kind('.* rule', ...)" }), { canPickMany: false, }, @@ -354,7 +354,7 @@ async function bazelTestTarget(adapter: IBazelCommandAdapter | undefined) { // invoked via the command palatte. Provide quickpick test targets for // the user to choose from. const quickPick = await vscode.window.showQuickPick( - queryQuickPickTargets("kind('.*_test rule', ...)"), + queryQuickPickTargets({ query: "kind('.*_test rule', ...)" }), { canPickMany: false, }, @@ -403,7 +403,7 @@ async function testPackage( // invoked via the command palatte. Provide quickpick build targets for // the user to choose from. const quickPick = await vscode.window.showQuickPick( - queryQuickPickPackage(), + queryQuickPickPackage({}), { canPickMany: false, },