Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Introduce pickTarget and pickPackage command variables #354

Merged
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
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 {
cameron-martin marked this conversation as resolved.
Show resolved Hide resolved
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
Loading