| import * as vscode from "vscode"; | |
| import type * as lc from "vscode-languageclient"; | |
| import * as ra from "./lsp_ext"; | |
| import * as tasks from "./tasks"; | |
| import type { CtxInit } from "./ctx"; | |
| import { makeDebugConfig } from "./debug"; | |
| import type { Config } from "./config"; | |
| import type { LanguageClient } from "vscode-languageclient/node"; | |
| import { Env, log, unwrapUndefinable, type RustEditor } from "./util"; | |
| const quickPickButtons = [ | |
| { iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configuration." }, | |
| ]; | |
| export async function selectRunnable( | |
| ctx: CtxInit, | |
| prevRunnable?: RunnableQuickPick, | |
| debuggeeOnly = false, | |
| showButtons: boolean = true, | |
| mode?: "cursor", | |
| ): Promise<RunnableQuickPick | undefined> { | |
| const editor = ctx.activeRustEditor ?? ctx.activeCargoTomlEditor; | |
| if (!editor) return; | |
| if (mode === "cursor") { | |
| return selectRunnableAtCursor(ctx, editor, prevRunnable); | |
| } | |
| // show a placeholder while we get the runnables from the server | |
| const quickPick = vscode.window.createQuickPick(); | |
| quickPick.title = "Select Runnable"; | |
| if (showButtons) { | |
| quickPick.buttons = quickPickButtons; | |
| } | |
| quickPick.items = [{ label: "Looking for runnables..." }]; | |
| quickPick.activeItems = []; | |
| quickPick.show(); | |
| const runnables = await getRunnables(ctx.client, editor, prevRunnable, debuggeeOnly); | |
| if (runnables.length === 0) { | |
| // it is the debug case, run always has at least 'cargo check ...' | |
| // see crates\rust-analyzer\src\handlers\request.rs, handle_runnables | |
| await vscode.window.showErrorMessage("There's no debug target!"); | |
| quickPick.dispose(); | |
| return; | |
| } | |
| // clear the list before we hook up listeners to avoid invoking them | |
| // if the user happens to accept the placeholder item | |
| quickPick.items = []; | |
| return await populateAndGetSelection( | |
| quickPick as vscode.QuickPick<RunnableQuickPick>, | |
| runnables, | |
| ctx, | |
| showButtons, | |
| ); | |
| } | |
| async function selectRunnableAtCursor( | |
| ctx: CtxInit, | |
| editor: RustEditor, | |
| prevRunnable?: RunnableQuickPick, | |
| ): Promise<RunnableQuickPick | undefined> { | |
| const runnableQuickPicks = await getRunnables(ctx.client, editor, prevRunnable, false); | |
| let runnableQuickPickAtCursor = null; | |
| const cursorPosition = ctx.client.code2ProtocolConverter.asPosition(editor.selection.active); | |
| for (const runnableQuickPick of runnableQuickPicks) { | |
| if (!runnableQuickPick.runnable.location?.targetRange) { | |
| continue; | |
| } | |
| const runnableQuickPickRange = runnableQuickPick.runnable.location.targetRange; | |
| if ( | |
| runnableQuickPickAtCursor?.runnable?.location?.targetRange != null && | |
| rangeContainsOtherRange( | |
| runnableQuickPickRange, | |
| runnableQuickPickAtCursor.runnable.location.targetRange, | |
| ) | |
| ) { | |
| continue; | |
| } | |
| if (rangeContainsPosition(runnableQuickPickRange, cursorPosition)) { | |
| runnableQuickPickAtCursor = runnableQuickPick; | |
| } | |
| } | |
| if (runnableQuickPickAtCursor == null) { | |
| return; | |
| } | |
| return Promise.resolve(runnableQuickPickAtCursor); | |
| } | |
| function rangeContainsPosition(range: lc.Range, position: lc.Position): boolean { | |
| return ( | |
| (position.line > range.start.line || | |
| (position.line === range.start.line && position.character >= range.start.character)) && | |
| (position.line < range.end.line || | |
| (position.line === range.end.line && position.character <= range.end.character)) | |
| ); | |
| } | |
| function rangeContainsOtherRange(range: lc.Range, otherRange: lc.Range) { | |
| return ( | |
| (range.start.line < otherRange.start.line || | |
| (range.start.line === otherRange.start.line && | |
| range.start.character <= otherRange.start.character)) && | |
| (range.end.line > otherRange.end.line || | |
| (range.end.line === otherRange.end.line && | |
| range.end.character >= otherRange.end.character)) | |
| ); | |
| } | |
| export class RunnableQuickPick implements vscode.QuickPickItem { | |
| public label: string; | |
| public description?: string | undefined; | |
| public detail?: string | undefined; | |
| public picked?: boolean | undefined; | |
| constructor(public runnable: ra.Runnable) { | |
| this.label = runnable.label; | |
| } | |
| } | |
| export function prepareBaseEnv(inheritEnv: boolean, base?: Env): Env { | |
| const env: Env = { RUST_BACKTRACE: "short" }; | |
| if (inheritEnv) { | |
| Object.assign(env, process.env); | |
| } | |
| if (base) { | |
| Object.assign(env, base); | |
| } | |
| return env; | |
| } | |
| export function prepareEnv(inheritEnv: boolean, runnableEnv?: Env, runnableEnvCfg?: Env): Env { | |
| const env = prepareBaseEnv(inheritEnv, runnableEnv); | |
| if (runnableEnvCfg) { | |
| Object.assign(env, runnableEnvCfg); | |
| } | |
| return env; | |
| } | |
| export async function createTaskFromRunnable( | |
| runnable: ra.Runnable, | |
| config: Config, | |
| ): Promise<vscode.Task> { | |
| const target = vscode.workspace.workspaceFolders?.[0]; | |
| let definition: tasks.TaskDefinition; | |
| let options; | |
| let cargo = "cargo"; | |
| if (runnable.kind === "cargo") { | |
| const runnableArgs = runnable.args; | |
| let args = createCargoArgs(runnableArgs); | |
| if (runnableArgs.overrideCargo) { | |
| // Split on spaces to allow overrides like "wrapper cargo". | |
| const cargoParts = runnableArgs.overrideCargo.split(" "); | |
| cargo = unwrapUndefinable(cargoParts[0]); | |
| args = [...cargoParts.slice(1), ...args]; | |
| } | |
| definition = { | |
| type: tasks.CARGO_TASK_TYPE, | |
| command: unwrapUndefinable(args[0]), | |
| args: args.slice(1), | |
| }; | |
| options = { | |
| cwd: runnableArgs.workspaceRoot || ".", | |
| env: prepareEnv( | |
| true, | |
| runnableArgs.environment, | |
| config.runnablesExtraEnv(runnable.label), | |
| ), | |
| }; | |
| } else { | |
| const runnableArgs = runnable.args; | |
| definition = { | |
| type: tasks.SHELL_TASK_TYPE, | |
| command: runnableArgs.program, | |
| args: runnableArgs.args, | |
| }; | |
| options = { | |
| cwd: runnableArgs.cwd, | |
| env: prepareBaseEnv(true), | |
| }; | |
| } | |
| const exec = await tasks.targetToExecution(definition, options, cargo); | |
| const task = await tasks.buildRustTask( | |
| target, | |
| definition, | |
| runnable.label, | |
| config.problemMatcher, | |
| exec, | |
| ); | |
| task.presentationOptions.clear = true; | |
| // Sadly, this doesn't prevent focus stealing if the terminal is currently | |
| // hidden, and will become revealed due to task execution. | |
| task.presentationOptions.focus = false; | |
| return task; | |
| } | |
| export function createCargoArgs(runnableArgs: ra.CargoRunnableArgs): string[] { | |
| const args = [...runnableArgs.cargoArgs]; // should be a copy! | |
| if (runnableArgs.executableArgs.length > 0) { | |
| args.push("--", ...runnableArgs.executableArgs); | |
| } | |
| return args; | |
| } | |
| async function getRunnables( | |
| client: LanguageClient, | |
| editor: RustEditor, | |
| prevRunnable?: RunnableQuickPick, | |
| debuggeeOnly = false, | |
| ): Promise<RunnableQuickPick[]> { | |
| const textDocument: lc.TextDocumentIdentifier = { | |
| uri: editor.document.uri.toString(), | |
| }; | |
| const runnables = await client | |
| .sendRequest(ra.runnables, { | |
| textDocument, | |
| position: client.code2ProtocolConverter.asPosition(editor.selection.active), | |
| }) | |
| .catch((err) => { | |
| // If this command is run for a virtual manifest at the workspace root, then this request | |
| // will fail as we do not watch this file. | |
| log.error(`${err}`); | |
| return []; | |
| }); | |
| const items: RunnableQuickPick[] = []; | |
| if (prevRunnable) { | |
| items.push(prevRunnable); | |
| } | |
| for (const r of runnables) { | |
| if (prevRunnable && JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)) { | |
| continue; | |
| } | |
| if (debuggeeOnly && r.label.startsWith("doctest")) { | |
| continue; | |
| } | |
| items.push(new RunnableQuickPick(r)); | |
| } | |
| return items; | |
| } | |
| async function populateAndGetSelection( | |
| quickPick: vscode.QuickPick<RunnableQuickPick>, | |
| runnables: RunnableQuickPick[], | |
| ctx: CtxInit, | |
| showButtons: boolean, | |
| ): Promise<RunnableQuickPick | undefined> { | |
| return new Promise((resolve) => { | |
| const disposables: vscode.Disposable[] = []; | |
| const close = (result?: RunnableQuickPick) => { | |
| resolve(result); | |
| disposables.forEach((d) => d.dispose()); | |
| }; | |
| disposables.push( | |
| quickPick.onDidHide(() => close()), | |
| quickPick.onDidAccept(() => close(quickPick.selectedItems[0] as RunnableQuickPick)), | |
| quickPick.onDidTriggerButton(async (_button) => { | |
| const runnable = unwrapUndefinable( | |
| quickPick.activeItems[0] as RunnableQuickPick, | |
| ).runnable; | |
| await makeDebugConfig(ctx, runnable); | |
| close(); | |
| }), | |
| quickPick.onDidChangeActive((activeList) => { | |
| if (showButtons && activeList.length > 0) { | |
| const active = unwrapUndefinable(activeList[0]); | |
| if (active.label.startsWith("cargo")) { | |
| // save button makes no sense for `cargo test` or `cargo check` | |
| quickPick.buttons = []; | |
| } else if (quickPick.buttons.length === 0) { | |
| quickPick.buttons = quickPickButtons; | |
| } | |
| } | |
| }), | |
| quickPick, | |
| ); | |
| // populate the list with the actual runnables | |
| quickPick.items = runnables; | |
| }); | |
| } |