Repository: rust-analyzer/rust-analyzer
Pull request #5954 (2020-09-05 14:06:58)

Add flexible configuration for runnables
This PR introduces two new configuration options for runnables: `overrideCargo` and `cargoExtraArgs`.
These options are applied to all the "run" tasks of rust analyzer, such as binaries and tests.

Overall motivation is that rust-analyzer provides similar options, for example, for `rustfmt`, but not for runnables.

## `overrideCargo`

This option allows user to replace `cargo` command with something else (well, something that is compatible with the cargo arguments).

Motivation is that some projects may have wrappers around cargo (or even whole alternatives to cargo), which do something related to the project, and only then run `cargo`. With this feature, such users will be able to use lens and run tests directly from the IDE rather than from terminal.

![cargo_override](https://user-images.githubusercontent.com/12111581/92306622-2f404f80-ef99-11ea-9bb7-6c6192a2c54a.gif)

## `cargoExtraArgs`

This option allows user to add any additional arguments for `cargo`, such as `--release`.

It may be useful, for example, if project has big integration tests which take too long in debug mode, or if any other `cargo` flag has to be passed.

![cargo_extra_args](https://user-images.githubusercontent.com/12111581/92306658-821a0700-ef99-11ea-8be9-bf0aff78e154.gif)

matklad: LGTM, but needs a rebase

while we are at it, maybe add support for env variables as well? 

bors d+

Long-term, I kind of think we should pursue "template" configurations here, but this ideally needs upstream support: https://github.com/microsoft/language-server-protocol/issues/944

bors[bot]: :v: popzxc can now approve this pull request. To approve and merge a pull request, simply reply with `bors r+`. More detailed instructions are available [here](https://bors.tech/documentation/getting-started/#reviewing-pull-requests).

Patch:
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 42e1ad376c..0ab4c37bff 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -38,6 +38,7 @@ pub struct Config {
     pub cargo: CargoConfig,
     pub rustfmt: RustfmtConfig,
     pub flycheck: Option<FlycheckConfig>,
+    pub runnables: RunnablesConfig,
 
     pub inlay_hints: InlayHintsConfig,
     pub completion: CompletionConfig,
@@ -124,6 +125,15 @@ pub enum RustfmtConfig {
     CustomCommand { command: String, args: Vec<String> },
 }
 
+/// Configuration for runnable items, such as `main` function or tests.
+#[derive(Debug, Clone, Default)]
+pub struct RunnablesConfig {
+    /// Custom command to be executed instead of `cargo` for runnables.
+    pub override_cargo: Option<String>,
+    /// Additional arguments for the `cargo`, e.g. `--release`.
+    pub cargo_extra_args: Vec<String>,
+}
+
 #[derive(Debug, Clone, Default)]
 pub struct ClientCapsConfig {
     pub location_link: bool,
@@ -164,6 +174,7 @@ impl Config {
                 extra_args: Vec::new(),
                 features: Vec::new(),
             }),
+            runnables: RunnablesConfig::default(),
 
             inlay_hints: InlayHintsConfig {
                 type_hints: true,
@@ -220,6 +231,10 @@ impl Config {
             load_out_dirs_from_check: data.cargo_loadOutDirsFromCheck,
             target: data.cargo_target.clone(),
         };
+        self.runnables = RunnablesConfig {
+            override_cargo: data.runnables_overrideCargo,
+            cargo_extra_args: data.runnables_cargoExtraArgs,
+        };
 
         self.proc_macro_srv = if data.procMacro_enable {
             std::env::current_exe().ok().map(|path| (path, vec!["proc-macro".into()]))
@@ -474,6 +489,9 @@ config_data! {
         notifications_cargoTomlNotFound: bool      = true,
         procMacro_enable: bool                     = false,
 
+        runnables_overrideCargo: Option<String> = None,
+        runnables_cargoExtraArgs: Vec<String>   = Vec::new(),
+
         rustfmt_extraArgs: Vec<String>               = Vec::new(),
         rustfmt_overrideCommand: Option<Vec<String>> = None,
 
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index afcec63ad1..e970abb7c0 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -491,6 +491,7 @@ pub(crate) fn handle_runnables(
     }
 
     // Add `cargo check` and `cargo test` for all targets of the whole package
+    let config = &snap.config.runnables;
     match cargo_spec {
         Some(spec) => {
             for &cmd in ["check", "test"].iter() {
@@ -500,12 +501,14 @@ pub(crate) fn handle_runnables(
                     kind: lsp_ext::RunnableKind::Cargo,
                     args: lsp_ext::CargoRunnable {
                         workspace_root: Some(spec.workspace_root.clone().into()),
+                        override_cargo: config.override_cargo.clone(),
                         cargo_args: vec![
                             cmd.to_string(),
                             "--package".to_string(),
                             spec.package.clone(),
                             "--all-targets".to_string(),
                         ],
+                        cargo_extra_args: config.cargo_extra_args.clone(),
                         executable_args: Vec::new(),
                         expect_test: None,
                     },
@@ -519,7 +522,9 @@ pub(crate) fn handle_runnables(
                 kind: lsp_ext::RunnableKind::Cargo,
                 args: lsp_ext::CargoRunnable {
                     workspace_root: None,
+                    override_cargo: config.override_cargo.clone(),
                     cargo_args: vec!["check".to_string(), "--workspace".to_string()],
+                    cargo_extra_args: config.cargo_extra_args.clone(),
                     executable_args: Vec::new(),
                     expect_test: None,
                 },
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 43ff191dab..fee0bb69c7 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -171,10 +171,14 @@ pub enum RunnableKind {
 #[derive(Deserialize, Serialize, Debug)]
 #[serde(rename_all = "camelCase")]
 pub struct CargoRunnable {
+    // command to be executed instead of cargo
+    pub override_cargo: Option<String>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub workspace_root: Option<PathBuf>,
     // command, --package and --lib stuff
     pub cargo_args: Vec<String>,
+    // user-specified additional cargo args, like `--release`.
+    pub cargo_extra_args: Vec<String>,
     // stuff after --
     pub executable_args: Vec<String>,
     #[serde(skip_serializing_if = "Option::is_none")]
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index 59e780b7d2..aeacde0f7a 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -740,6 +740,7 @@ pub(crate) fn runnable(
     file_id: FileId,
     runnable: Runnable,
 ) -> Result<lsp_ext::Runnable> {
+    let config = &snap.config.runnables;
     let spec = CargoTargetSpec::for_file(snap, file_id)?;
     let workspace_root = spec.as_ref().map(|it| it.workspace_root.clone());
     let target = spec.as_ref().map(|s| s.target.clone());
@@ -754,7 +755,9 @@ pub(crate) fn runnable(
         kind: lsp_ext::RunnableKind::Cargo,
         args: lsp_ext::CargoRunnable {
             workspace_root: workspace_root.map(|it| it.into()),
+            override_cargo: config.override_cargo.clone(),
             cargo_args,
+            cargo_extra_args: config.cargo_extra_args.clone(),
             executable_args,
             expect_test: None,
         },
diff --git a/editors/code/package.json b/editors/code/package.json
index bdd8a0c298..cc2ac3bd26 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -651,6 +651,22 @@
                     ],
                     "default": "full",
                     "description": "The strategy to use when inserting new imports or merging imports."
+                },
+                "rust-analyzer.runnables.overrideCargo": {
+                    "type": [
+                        "null",
+                        "string"
+                    ],
+                    "default": null,
+                    "description": "Command to be executed instead of 'cargo' for runnables."
+                },
+                "rust-analyzer.runnables.cargoExtraArgs": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    },
+                    "default": [],
+                    "description": "Additional arguments to be passed to cargo for runnables such as tests or binaries.\nFor example, it may be '--release'"
                 }
             }
         },
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index eb422d3e75..f286b68a68 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -69,8 +69,10 @@ export interface Runnable {
     args: {
         workspaceRoot?: string;
         cargoArgs: string[];
+        cargoExtraArgs: string[];
         executableArgs: string[];
         expectTest?: boolean;
+        overrideCargo?: string;
     };
 }
 export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("experimental/runnables");
diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts
index de68f27aec..459b7f250d 100644
--- a/editors/code/src/run.ts
+++ b/editors/code/src/run.ts
@@ -129,6 +129,7 @@ export async function createTask(runnable: ra.Runnable, config: Config): Promise
     }
 
     const args = [...runnable.args.cargoArgs]; // should be a copy!
+    args.push(...runnable.args.cargoExtraArgs); // Append user-specified cargo options.
     if (runnable.args.executableArgs.length > 0) {
         args.push('--', ...runnable.args.executableArgs);
     }
@@ -139,6 +140,7 @@ export async function createTask(runnable: ra.Runnable, config: Config): Promise
         args: args.slice(1),
         cwd: runnable.args.workspaceRoot || ".",
         env: prepareEnv(runnable, config.runnableEnv),
+        overrideCargo: runnable.args.overrideCargo,
     };
 
     const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate()
diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts
index 14abbd5b79..a3ff151025 100644
--- a/editors/code/src/tasks.ts
+++ b/editors/code/src/tasks.ts
@@ -13,6 +13,7 @@ export interface CargoTaskDefinition extends vscode.TaskDefinition {
     args?: string[];
     cwd?: string;
     env?: { [key: string]: string };
+    overrideCargo?: string;
 }
 
 class CargoTaskProvider implements vscode.TaskProvider {
@@ -98,7 +99,14 @@ export async function buildCargoTask(
     }
 
     if (!exec) {
-        exec = new vscode.ShellExecution(toolchain.cargoPath(), args, definition);
+        // Check whether we must use a user-defined substitute for cargo.
+        const cargoCommand = definition.overrideCargo ? definition.overrideCargo : toolchain.cargoPath();
+
+        // Prepare the whole command as one line. It is required if user has provided override command which contains spaces,
+        // for example "wrapper cargo". Without manual preparation the overridden command will be quoted and fail to execute.
+        const fullCommand = [cargoCommand, ...args].join(" ");
+
+        exec = new vscode.ShellExecution(fullCommand, definition);
     }
 
     return new vscode.Task(


Test patch:
diff --git a/crates/rust-analyzer/tests/rust-analyzer/main.rs b/crates/rust-analyzer/tests/rust-analyzer/main.rs
index 06726f9576..e51eb26261 100644
--- a/crates/rust-analyzer/tests/rust-analyzer/main.rs
+++ b/crates/rust-analyzer/tests/rust-analyzer/main.rs
@@ -107,6 +107,8 @@ fn main() {}
             "args": {
               "cargoArgs": ["test", "--package", "foo", "--test", "spam"],
               "executableArgs": ["test_eggs", "--exact", "--nocapture"],
+              "cargoExtraArgs": [],
+              "overrideCargo": null,
               "workspaceRoot": server.path().join("foo")
             },
             "kind": "cargo",
@@ -127,6 +129,8 @@ fn main() {}
             "args": {
               "cargoArgs": ["check", "--package", "foo", "--all-targets"],
               "executableArgs": [],
+              "cargoExtraArgs": [],
+              "overrideCargo": null,
               "workspaceRoot": server.path().join("foo")
             },
             "kind": "cargo",
@@ -136,6 +140,8 @@ fn main() {}
             "args": {
               "cargoArgs": ["test", "--package", "foo", "--all-targets"],
               "executableArgs": [],
+              "cargoExtraArgs": [],
+              "overrideCargo": null,
               "workspaceRoot": server.path().join("foo")
             },
             "kind": "cargo",
diff --git a/editors/code/tests/unit/runnable_env.test.ts b/editors/code/tests/unit/runnable_env.test.ts
index f2f53e91ad..c5600cf64f 100644
--- a/editors/code/tests/unit/runnable_env.test.ts
+++ b/editors/code/tests/unit/runnable_env.test.ts
@@ -9,7 +9,8 @@ function makeRunnable(label: string): ra.Runnable {
         kind: "cargo",
         args: {
             cargoArgs: [],
-            executableArgs: []
+            executableArgs: [],
+            cargoExtraArgs: []
         }
     };
 }