diff --git a/editors/code/package.json b/editors/code/package.json index 68484a370b..f542a490a9 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -336,6 +336,14 @@ "default": null, "description": "List of features to activate. Defaults to `rust-analyzer.cargo.features`." }, + "rust-analyzer.cargoRunner": { + "type": [ + "null", + "string" + ], + "default": null, + "description": "Custom cargo runner extension ID." + }, "rust-analyzer.inlayHints.enable": { "type": "boolean", "default": true, diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index 48a25495fb..8c9d7802ff 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -394,7 +394,7 @@ export function run(ctx: Ctx): Cmd { item.detail = 'rerun'; prevRunnable = item; - const task = createTask(item.runnable); + const task = await createTask(item.runnable, ctx.config); return await vscode.tasks.executeTask(task); }; } @@ -404,7 +404,7 @@ export function runSingle(ctx: Ctx): Cmd { const editor = ctx.activeRustEditor; if (!editor) return; - const task = createTask(runnable); + const task = await createTask(runnable, ctx.config); task.group = vscode.TaskGroup.Build; task.presentationOptions = { reveal: vscode.TaskRevealKind.Always, diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 9591d4fe32..fc95a7de6b 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -110,6 +110,10 @@ export class Config { }; } + get cargoRunner() { + return this.get("cargoRunner"); + } + get debug() { // "/rustc/" used by suggestions only. const { ["/rustc/"]: _, ...sourceFileMap } = this.get>("debug.sourceFileMap"); diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index cdb63b46f9..5ceab8b44e 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -114,7 +114,7 @@ export async function activate(context: vscode.ExtensionContext) { ctx.registerCommand('applyActionGroup', commands.applyActionGroup); ctx.registerCommand('gotoLocation', commands.gotoLocation); - ctx.pushCleanup(activateTaskProvider(workspaceFolder)); + ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config)); activateInlayHints(ctx); diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts index bb060cfe15..766b051126 100644 --- a/editors/code/src/run.ts +++ b/editors/code/src/run.ts @@ -1,10 +1,11 @@ import * as vscode from 'vscode'; import * as lc from 'vscode-languageclient'; import * as ra from './lsp_ext'; -import * as toolchain from "./toolchain"; +import * as tasks from './tasks'; import { Ctx } from './ctx'; import { makeDebugConfig } from './debug'; +import { Config } from './config'; const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }]; @@ -95,52 +96,29 @@ export class RunnableQuickPick implements vscode.QuickPickItem { } } -interface CargoTaskDefinition extends vscode.TaskDefinition { - type: 'cargo'; - label: string; - command: string; - args: string[]; - env?: { [key: string]: string }; -} +export async function createTask(runnable: ra.Runnable, config: Config): Promise { + if (runnable.kind !== "cargo") { + // rust-analyzer supports only one kind, "cargo" + // do not use tasks.TASK_TYPE here, these are completely different meanings. -export function createTask(runnable: ra.Runnable): vscode.Task { - const TASK_SOURCE = 'Rust'; - - let command; - switch (runnable.kind) { - case "cargo": command = toolchain.getPathForExecutable("cargo"); + throw `Unexpected runnable kind: ${runnable.kind}`; } + const args = [...runnable.args.cargoArgs]; // should be a copy! if (runnable.args.executableArgs.length > 0) { args.push('--', ...runnable.args.executableArgs); } - const definition: CargoTaskDefinition = { - type: 'cargo', - label: runnable.label, - command, - args, + const definition: tasks.CargoTaskDefinition = { + type: tasks.TASK_TYPE, + command: args[0], // run, test, etc... + args: args.slice(1), + cwd: runnable.args.workspaceRoot, env: Object.assign({}, process.env as { [key: string]: string }, { "RUST_BACKTRACE": "short" }), }; - const execOption: vscode.ShellExecutionOptions = { - cwd: runnable.args.workspaceRoot || '.', - env: definition.env, - }; - const exec = new vscode.ShellExecution( - definition.command, - definition.args, - execOption, - ); + const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate() + const cargoTask = await tasks.buildCargoTask(target, definition, runnable.label, args, config.cargoRunner, true); + cargoTask.presentationOptions.clear = true; - const f = vscode.workspace.workspaceFolders![0]; - const t = new vscode.Task( - definition, - f, - definition.label, - TASK_SOURCE, - exec, - ['$rustc'], - ); - t.presentationOptions.clear = true; - return t; + return cargoTask; } diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts index 9748824df3..14abbd5b79 100644 --- a/editors/code/src/tasks.ts +++ b/editors/code/src/tasks.ts @@ -1,11 +1,14 @@ import * as vscode from 'vscode'; import * as toolchain from "./toolchain"; +import { Config } from './config'; +import { log } from './util'; // This ends up as the `type` key in tasks.json. RLS also uses `cargo` and // our configuration should be compatible with it so use the same key. -const TASK_TYPE = 'cargo'; +export const TASK_TYPE = 'cargo'; +export const TASK_SOURCE = 'rust'; -interface CargoTaskDefinition extends vscode.TaskDefinition { +export interface CargoTaskDefinition extends vscode.TaskDefinition { command?: string; args?: string[]; cwd?: string; @@ -14,73 +17,101 @@ interface CargoTaskDefinition extends vscode.TaskDefinition { class CargoTaskProvider implements vscode.TaskProvider { private readonly target: vscode.WorkspaceFolder; + private readonly config: Config; - constructor(target: vscode.WorkspaceFolder) { + constructor(target: vscode.WorkspaceFolder, config: Config) { this.target = target; + this.config = config; } - provideTasks(): vscode.Task[] { + async provideTasks(): Promise { // Detect Rust tasks. Currently we do not do any actual detection // of tasks (e.g. aliases in .cargo/config) and just return a fixed // set of tasks that always exist. These tasks cannot be removed in // tasks.json - only tweaked. - const cargoPath = toolchain.cargoPath(); - - return [ + const defs = [ { command: 'build', group: vscode.TaskGroup.Build }, { command: 'check', group: vscode.TaskGroup.Build }, { command: 'test', group: vscode.TaskGroup.Test }, { command: 'clean', group: vscode.TaskGroup.Clean }, { command: 'run', group: undefined }, - ] - .map(({ command, group }) => { - const vscodeTask = new vscode.Task( - // The contents of this object end up in the tasks.json entries. - { - type: TASK_TYPE, - command, - }, - // The scope of the task - workspace or specific folder (global - // is not supported). - this.target, - // The task name, and task source. These are shown in the UI as - // `${source}: ${name}`, e.g. `rust: cargo build`. - `cargo ${command}`, - 'rust', - // What to do when this command is executed. - new vscode.ShellExecution(cargoPath, [command]), - // Problem matchers. - ['$rustc'], - ); - vscodeTask.group = group; - return vscodeTask; - }); + ]; + + const tasks: vscode.Task[] = []; + for (const def of defs) { + const vscodeTask = await buildCargoTask(this.target, { type: TASK_TYPE, command: def.command }, `cargo ${def.command}`, [def.command], this.config.cargoRunner); + vscodeTask.group = def.group; + tasks.push(vscodeTask); + } + + return tasks; } - resolveTask(task: vscode.Task): vscode.Task | undefined { + async resolveTask(task: vscode.Task): Promise { // VSCode calls this for every cargo task in the user's tasks.json, // we need to inform VSCode how to execute that command by creating // a ShellExecution for it. const definition = task.definition as CargoTaskDefinition; - if (definition.type === 'cargo' && definition.command) { + if (definition.type === TASK_TYPE && definition.command) { const args = [definition.command].concat(definition.args ?? []); - return new vscode.Task( - definition, - task.name, - 'rust', - new vscode.ShellExecution('cargo', args, definition), - ); + return await buildCargoTask(this.target, definition, task.name, args, this.config.cargoRunner); } return undefined; } } -export function activateTaskProvider(target: vscode.WorkspaceFolder): vscode.Disposable { - const provider = new CargoTaskProvider(target); +export async function buildCargoTask( + target: vscode.WorkspaceFolder, + definition: CargoTaskDefinition, + name: string, + args: string[], + customRunner?: string, + throwOnError: boolean = false +): Promise { + + let exec: vscode.ShellExecution | undefined = undefined; + + if (customRunner) { + const runnerCommand = `${customRunner}.buildShellExecution`; + try { + const runnerArgs = { kind: TASK_TYPE, args, cwd: definition.cwd, env: definition.env }; + const customExec = await vscode.commands.executeCommand(runnerCommand, runnerArgs); + if (customExec) { + if (customExec instanceof vscode.ShellExecution) { + exec = customExec; + } else { + log.debug("Invalid cargo ShellExecution", customExec); + throw "Invalid cargo ShellExecution."; + } + } + // fallback to default processing + + } catch (e) { + if (throwOnError) throw `Cargo runner '${customRunner}' failed! ${e}`; + // fallback to default processing + } + } + + if (!exec) { + exec = new vscode.ShellExecution(toolchain.cargoPath(), args, definition); + } + + return new vscode.Task( + definition, + target, + name, + TASK_SOURCE, + exec, + ['$rustc'] + ); +} + +export function activateTaskProvider(target: vscode.WorkspaceFolder, config: Config): vscode.Disposable { + const provider = new CargoTaskProvider(target, config); return vscode.tasks.registerTaskProvider(TASK_TYPE, provider); }