Repository: rust-analyzer/rust-analyzer
Pull request #5017 (2020-06-24 08:21:14)

Add custom cargo runners support.
This PR adds an option to delegate actual cargo commands building to another extension. For example, to use a different manager like [cross](https://github.com/rust-embedded/cross).

https://github.com/vsrs/cross-rust-analyzer is an example of such extension. I'll publish it after the rust-analyzer release with this functionality.

Fixes https://github.com/rust-analyzer/rust-analyzer/issues/4902


Patch:
diff --git a/crates/ra_db/src/fixture.rs b/crates/ra_db/src/fixture.rs
index 4f4fb44943..ddf46e6c44 100644
--- a/crates/ra_db/src/fixture.rs
+++ b/crates/ra_db/src/fixture.rs
@@ -132,17 +132,10 @@ fn with_files(
 
     let mut file_position = None;
 
-    for entry in fixture {
-        let text = if entry.text.contains(CURSOR_MARKER) {
-            let (range_or_offset, text) = extract_range_or_offset(&entry.text);
-            assert!(file_position.is_none());
-            file_position = Some((file_id, range_or_offset));
-            text.to_string()
-        } else {
-            entry.text.clone()
+    for entry in fixture.iter() {
+        let meta = match ParsedMeta::from(entry) {
+            ParsedMeta::File(it) => it,
         };
-
-        let meta = FileMeta::from(entry);
         assert!(meta.path.starts_with(&source_root_prefix));
 
         if let Some(krate) = meta.krate {
@@ -164,6 +157,15 @@ fn with_files(
             default_crate_root = Some(file_id);
         }
 
+        let text = if entry.text.contains(CURSOR_MARKER) {
+            let (range_or_offset, text) = extract_range_or_offset(&entry.text);
+            assert!(file_position.is_none());
+            file_position = Some((file_id, range_or_offset));
+            text.to_string()
+        } else {
+            entry.text.to_string()
+        };
+
         db.set_file_text(file_id, Arc::new(text));
         db.set_file_source_root(file_id, source_root_id);
         let path = VfsPath::new_virtual_path(meta.path);
@@ -196,6 +198,10 @@ fn with_files(
     (file_position, files)
 }
 
+enum ParsedMeta {
+    File(FileMeta),
+}
+
 struct FileMeta {
     path: String,
     krate: Option<String>,
@@ -205,22 +211,22 @@ struct FileMeta {
     env: Env,
 }
 
-impl From<Fixture> for FileMeta {
-    fn from(f: Fixture) -> FileMeta {
+impl From<&Fixture> for ParsedMeta {
+    fn from(f: &Fixture) -> Self {
         let mut cfg = CfgOptions::default();
         f.cfg_atoms.iter().for_each(|it| cfg.insert_atom(it.into()));
         f.cfg_key_values.iter().for_each(|(k, v)| cfg.insert_key_value(k.into(), v.into()));
 
-        FileMeta {
-            path: f.path,
-            krate: f.krate,
-            deps: f.deps,
+        Self::File(FileMeta {
+            path: f.path.to_owned(),
+            krate: f.crate_name.to_owned(),
+            deps: f.deps.to_owned(),
             cfg,
             edition: f
                 .edition
                 .as_ref()
                 .map_or(Edition::Edition2018, |v| Edition::from_str(&v).unwrap()),
             env: Env::from(f.env.iter()),
-        }
+        })
     }
 }
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<string | undefined>("cargoRunner");
+    }
+
     get debug() {
         // "/rustc/<id>" used by suggestions only.
         const { ["/rustc/<id>"]: _, ...sourceFileMap } = this.get<Record<string, string>>("debug.sourceFileMap");
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 12b4d05108..5b4f453c86 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -115,7 +115,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));
 
     activateStatusDisplay(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 function createTask(runnable: ra.Runnable): vscode.Task {
-    const TASK_SOURCE = 'Rust';
+export async function createTask(runnable: ra.Runnable, config: Config): Promise<vscode.Task> {
+    if (runnable.kind !== "cargo") {
+        // rust-analyzer supports only one kind, "cargo"
+        // do not use tasks.TASK_TYPE here, these are completely different meanings.
 
-    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 f = vscode.workspace.workspaceFolders![0];
-    const t = new vscode.Task(
-        definition,
-        f,
-        definition.label,
-        TASK_SOURCE,
-        exec,
-        ['$rustc'],
-    );
-    t.presentationOptions.clear = true;
-    return t;
+    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;
+
+    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<vscode.Task[]> {
         // 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.Task | undefined> {
         // 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<vscode.Task> {
+
+    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);
 }


Test patch:
diff --git a/crates/test_utils/src/fixture.rs b/crates/test_utils/src/fixture.rs
index fad8f7e2cc..7e93fbcd64 100644
--- a/crates/test_utils/src/fixture.rs
+++ b/crates/test_utils/src/fixture.rs
@@ -8,7 +8,7 @@ use stdx::{lines_with_ends, split_delim, trim_indent};
 pub struct Fixture {
     pub path: String,
     pub text: String,
-    pub krate: Option<String>,
+    pub crate_name: Option<String>,
     pub deps: Vec<String>,
     pub cfg_atoms: Vec<String>,
     pub cfg_key_values: Vec<(String, String)>,
@@ -56,7 +56,7 @@ impl Fixture {
     }
 
     //- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b env:OUTDIR=path/to,OTHER=foo
-    fn parse_meta_line(meta: &str) -> Fixture {
+    pub fn parse_meta_line(meta: &str) -> Fixture {
         assert!(meta.starts_with("//-"));
         let meta = meta["//-".len()..].trim();
         let components = meta.split_ascii_whitespace().collect::<Vec<_>>();
@@ -98,7 +98,7 @@ impl Fixture {
         Fixture {
             path,
             text: String::new(),
-            krate: krate,
+            crate_name: krate,
             deps,
             cfg_atoms,
             cfg_key_values,
@@ -136,7 +136,7 @@ fn parse_fixture_gets_full_meta() {
     let meta = &parsed[0];
     assert_eq!("mod m;\n", meta.text);
 
-    assert_eq!("foo", meta.krate.as_ref().unwrap());
+    assert_eq!("foo", meta.crate_name.as_ref().unwrap());
     assert_eq!("/lib.rs", meta.path);
     assert_eq!(2, meta.env.len());
 }