aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/assists/src/utils/insert_use.rs133
-rw-r--r--crates/rust-analyzer/src/config.rs18
-rw-r--r--crates/rust-analyzer/src/handlers.rs5
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs4
-rw-r--r--crates/rust-analyzer/src/to_proto.rs3
-rw-r--r--crates/rust-analyzer/tests/rust-analyzer/main.rs6
-rw-r--r--editors/code/package.json16
-rw-r--r--editors/code/src/lsp_ext.ts2
-rw-r--r--editors/code/src/run.ts2
-rw-r--r--editors/code/src/tasks.ts10
-rw-r--r--editors/code/tests/unit/runnable_env.test.ts3
11 files changed, 152 insertions, 50 deletions
diff --git a/crates/assists/src/utils/insert_use.rs b/crates/assists/src/utils/insert_use.rs
index 8dd4fe607..f6025c99a 100644
--- a/crates/assists/src/utils/insert_use.rs
+++ b/crates/assists/src/utils/insert_use.rs
@@ -4,13 +4,14 @@ use std::{
4 iter::{self, successors}, 4 iter::{self, successors},
5}; 5};
6 6
7use ast::{ 7use itertools::{EitherOrBoth, Itertools};
8 edit::{AstNodeEdit, IndentLevel},
9 PathSegmentKind, VisibilityOwner,
10};
11use syntax::{ 8use syntax::{
12 algo, 9 algo,
13 ast::{self, make, AstNode}, 10 ast::{
11 self,
12 edit::{AstNodeEdit, IndentLevel},
13 make, AstNode, PathSegmentKind, VisibilityOwner,
14 },
14 InsertPosition, SyntaxElement, SyntaxNode, 15 InsertPosition, SyntaxElement, SyntaxNode,
15}; 16};
16 17
@@ -193,13 +194,13 @@ fn recursive_merge(
193 false => None, 194 false => None,
194 }) 195 })
195 .collect::<Option<Vec<_>>>()?; 196 .collect::<Option<Vec<_>>>()?;
196 use_trees.sort_unstable_by(|a, b| path_cmp_opt(a.path(), b.path())); 197 use_trees.sort_unstable_by(|a, b| path_cmp_for_sort(a.path(), b.path()));
197 for rhs_t in rhs.use_tree_list().into_iter().flat_map(|list| list.use_trees()) { 198 for rhs_t in rhs.use_tree_list().into_iter().flat_map(|list| list.use_trees()) {
198 if !merge.is_tree_allowed(&rhs_t) { 199 if !merge.is_tree_allowed(&rhs_t) {
199 return None; 200 return None;
200 } 201 }
201 let rhs_path = rhs_t.path(); 202 let rhs_path = rhs_t.path();
202 match use_trees.binary_search_by(|p| path_cmp_opt(p.path(), rhs_path.clone())) { 203 match use_trees.binary_search_by(|p| path_cmp_bin_search(p.path(), rhs_path.clone())) {
203 Ok(idx) => { 204 Ok(idx) => {
204 let lhs_t = &mut use_trees[idx]; 205 let lhs_t = &mut use_trees[idx];
205 let lhs_path = lhs_t.path()?; 206 let lhs_path = lhs_t.path()?;
@@ -307,39 +308,77 @@ fn path_len(path: ast::Path) -> usize {
307 308
308/// Orders paths in the following way: 309/// Orders paths in the following way:
309/// the sole self token comes first, after that come uppercase identifiers, then lowercase identifiers 310/// the sole self token comes first, after that come uppercase identifiers, then lowercase identifiers
310// FIXME: rustfmt sort lowercase idents before uppercase, in general we want to have the same ordering rustfmt has 311// FIXME: rustfmt sorts lowercase idents before uppercase, in general we want to have the same ordering rustfmt has
311// which is `self` and `super` first, then identifier imports with lowercase ones first, then glob imports and at last list imports. 312// which is `self` and `super` first, then identifier imports with lowercase ones first, then glob imports and at last list imports.
312// Example foo::{self, foo, baz, Baz, Qux, *, {Bar}} 313// Example foo::{self, foo, baz, Baz, Qux, *, {Bar}}
313fn path_cmp(a: &ast::Path, b: &ast::Path) -> Ordering { 314fn path_cmp_for_sort(a: Option<ast::Path>, b: Option<ast::Path>) -> Ordering {
314 match (path_is_self(a), path_is_self(b)) { 315 match (a, b) {
315 (true, true) => Ordering::Equal, 316 (None, None) => Ordering::Equal,
316 (true, false) => Ordering::Less, 317 (None, Some(_)) => Ordering::Less,
317 (false, true) => Ordering::Greater, 318 (Some(_), None) => Ordering::Greater,
318 (false, false) => { 319 (Some(ref a), Some(ref b)) => match (path_is_self(a), path_is_self(b)) {
319 let a = segment_iter(a); 320 (true, true) => Ordering::Equal,
320 let b = segment_iter(b); 321 (true, false) => Ordering::Less,
321 // cmp_by would be useful for us here but that is currently unstable 322 (false, true) => Ordering::Greater,
322 // cmp doesnt work due the lifetimes on text's return type 323 (false, false) => path_cmp_short(a, b),
323 a.zip(b) 324 },
324 .flat_map(|(seg, seg2)| seg.name_ref().zip(seg2.name_ref()))
325 .find_map(|(a, b)| match a.text().cmp(b.text()) {
326 ord @ Ordering::Greater | ord @ Ordering::Less => Some(ord),
327 Ordering::Equal => None,
328 })
329 .unwrap_or(Ordering::Equal)
330 }
331 } 325 }
332} 326}
333 327
334fn path_cmp_opt(a: Option<ast::Path>, b: Option<ast::Path>) -> Ordering { 328/// Path comparison func for binary searching for merging.
335 match (a, b) { 329fn path_cmp_bin_search(lhs: Option<ast::Path>, rhs: Option<ast::Path>) -> Ordering {
330 match (lhs, rhs) {
336 (None, None) => Ordering::Equal, 331 (None, None) => Ordering::Equal,
337 (None, Some(_)) => Ordering::Less, 332 (None, Some(_)) => Ordering::Less,
338 (Some(_), None) => Ordering::Greater, 333 (Some(_), None) => Ordering::Greater,
339 (Some(a), Some(b)) => path_cmp(&a, &b), 334 (Some(ref a), Some(ref b)) => path_cmp_short(a, b),
340 } 335 }
341} 336}
342 337
338/// Short circuiting comparison, if both paths are equal until one of them ends they are considered
339/// equal
340fn path_cmp_short(a: &ast::Path, b: &ast::Path) -> Ordering {
341 let a = segment_iter(a);
342 let b = segment_iter(b);
343 // cmp_by would be useful for us here but that is currently unstable
344 // cmp doesnt work due the lifetimes on text's return type
345 a.zip(b)
346 .find_map(|(a, b)| match path_segment_cmp(&a, &b) {
347 Ordering::Equal => None,
348 ord => Some(ord),
349 })
350 .unwrap_or(Ordering::Equal)
351}
352
353/// Compares to paths, if one ends earlier than the other the has_tl parameters decide which is
354/// greater as a a path that has a tree list should be greater, while one that just ends without
355/// a tree list should be considered less.
356fn use_tree_path_cmp(a: &ast::Path, a_has_tl: bool, b: &ast::Path, b_has_tl: bool) -> Ordering {
357 let a_segments = segment_iter(a);
358 let b_segments = segment_iter(b);
359 // cmp_by would be useful for us here but that is currently unstable
360 // cmp doesnt work due the lifetimes on text's return type
361 a_segments
362 .zip_longest(b_segments)
363 .find_map(|zipped| match zipped {
364 EitherOrBoth::Both(ref a, ref b) => match path_segment_cmp(a, b) {
365 Ordering::Equal => None,
366 ord => Some(ord),
367 },
368 EitherOrBoth::Left(_) if !b_has_tl => Some(Ordering::Greater),
369 EitherOrBoth::Left(_) => Some(Ordering::Less),
370 EitherOrBoth::Right(_) if !a_has_tl => Some(Ordering::Less),
371 EitherOrBoth::Right(_) => Some(Ordering::Greater),
372 })
373 .unwrap_or(Ordering::Equal)
374}
375
376fn path_segment_cmp(a: &ast::PathSegment, b: &ast::PathSegment) -> Ordering {
377 let a = a.name_ref();
378 let b = b.name_ref();
379 a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text))
380}
381
343/// What type of merges are allowed. 382/// What type of merges are allowed.
344#[derive(Copy, Clone, Debug, PartialEq, Eq)] 383#[derive(Copy, Clone, Debug, PartialEq, Eq)]
345pub enum MergeBehaviour { 384pub enum MergeBehaviour {
@@ -389,7 +428,6 @@ impl ImportGroup {
389 PathSegmentKind::Name(name) => match name.text().as_str() { 428 PathSegmentKind::Name(name) => match name.text().as_str() {
390 "std" => ImportGroup::Std, 429 "std" => ImportGroup::Std,
391 "core" => ImportGroup::Std, 430 "core" => ImportGroup::Std,
392 // FIXME: can be ThisModule as well
393 _ => ImportGroup::ExternCrate, 431 _ => ImportGroup::ExternCrate,
394 }, 432 },
395 PathSegmentKind::Type { .. } => unreachable!(), 433 PathSegmentKind::Type { .. } => unreachable!(),
@@ -415,30 +453,30 @@ fn find_insert_position(
415 .as_syntax_node() 453 .as_syntax_node()
416 .children() 454 .children()
417 .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node))) 455 .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
418 .flat_map(|(use_, node)| use_.use_tree().and_then(|tree| tree.path()).zip(Some(node))); 456 .flat_map(|(use_, node)| {
457 let tree = use_.use_tree()?;
458 let path = tree.path()?;
459 let has_tl = tree.use_tree_list().is_some();
460 Some((path, has_tl, node))
461 });
419 // Iterator that discards anything thats not in the required grouping 462 // Iterator that discards anything thats not in the required grouping
420 // This implementation allows the user to rearrange their import groups as this only takes the first group that fits 463 // This implementation allows the user to rearrange their import groups as this only takes the first group that fits
421 let group_iter = path_node_iter 464 let group_iter = path_node_iter
422 .clone() 465 .clone()
423 .skip_while(|(path, _)| ImportGroup::new(path) != group) 466 .skip_while(|(path, ..)| ImportGroup::new(path) != group)
424 .take_while(|(path, _)| ImportGroup::new(path) == group); 467 .take_while(|(path, ..)| ImportGroup::new(path) == group);
425 468
426 let segments = segment_iter(&insert_path);
427 // track the last element we iterated over, if this is still None after the iteration then that means we never iterated in the first place 469 // track the last element we iterated over, if this is still None after the iteration then that means we never iterated in the first place
428 let mut last = None; 470 let mut last = None;
429 // find the element that would come directly after our new import 471 // find the element that would come directly after our new import
430 let post_insert = 472 let post_insert = group_iter.inspect(|(.., node)| last = Some(node.clone())).find(
431 group_iter.inspect(|(_, node)| last = Some(node.clone())).find(|(path, _)| { 473 |&(ref path, has_tl, _)| {
432 let check_segments = segment_iter(&path); 474 use_tree_path_cmp(&insert_path, false, path, has_tl) != Ordering::Greater
433 segments 475 },
434 .clone() 476 );
435 .zip(check_segments)
436 .flat_map(|(seg, seg2)| seg.name_ref().zip(seg2.name_ref()))
437 .all(|(l, r)| l.text() <= r.text())
438 });
439 match post_insert { 477 match post_insert {
440 // insert our import before that element 478 // insert our import before that element
441 Some((_, node)) => (InsertPosition::Before(node.into()), AddBlankLine::After), 479 Some((.., node)) => (InsertPosition::Before(node.into()), AddBlankLine::After),
442 // there is no element after our new import, so append it to the end of the group 480 // there is no element after our new import, so append it to the end of the group
443 None => match last { 481 None => match last {
444 Some(node) => (InsertPosition::After(node.into()), AddBlankLine::Before), 482 Some(node) => (InsertPosition::After(node.into()), AddBlankLine::Before),
@@ -448,10 +486,10 @@ fn find_insert_position(
448 let mut last = None; 486 let mut last = None;
449 // find the group that comes after where we want to insert 487 // find the group that comes after where we want to insert
450 let post_group = path_node_iter 488 let post_group = path_node_iter
451 .inspect(|(_, node)| last = Some(node.clone())) 489 .inspect(|(.., node)| last = Some(node.clone()))
452 .find(|(p, _)| ImportGroup::new(p) > group); 490 .find(|(p, ..)| ImportGroup::new(p) > group);
453 match post_group { 491 match post_group {
454 Some((_, node)) => { 492 Some((.., node)) => {
455 (InsertPosition::Before(node.into()), AddBlankLine::AfterTwice) 493 (InsertPosition::Before(node.into()), AddBlankLine::AfterTwice)
456 } 494 }
457 // there is no such group, so append after the last one 495 // there is no such group, so append after the last one
@@ -844,7 +882,6 @@ use foo::bar::baz::Qux;",
844 } 882 }
845 883
846 #[test] 884 #[test]
847 #[ignore] // FIXME: Order changes when switching lhs and rhs
848 fn skip_merge_last_too_long2() { 885 fn skip_merge_last_too_long2() {
849 check_last( 886 check_last(
850 "foo::bar::baz::Qux", 887 "foo::bar::baz::Qux",
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 42e1ad376..0ab4c37bf 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -38,6 +38,7 @@ pub struct Config {
38 pub cargo: CargoConfig, 38 pub cargo: CargoConfig,
39 pub rustfmt: RustfmtConfig, 39 pub rustfmt: RustfmtConfig,
40 pub flycheck: Option<FlycheckConfig>, 40 pub flycheck: Option<FlycheckConfig>,
41 pub runnables: RunnablesConfig,
41 42
42 pub inlay_hints: InlayHintsConfig, 43 pub inlay_hints: InlayHintsConfig,
43 pub completion: CompletionConfig, 44 pub completion: CompletionConfig,
@@ -124,6 +125,15 @@ pub enum RustfmtConfig {
124 CustomCommand { command: String, args: Vec<String> }, 125 CustomCommand { command: String, args: Vec<String> },
125} 126}
126 127
128/// Configuration for runnable items, such as `main` function or tests.
129#[derive(Debug, Clone, Default)]
130pub struct RunnablesConfig {
131 /// Custom command to be executed instead of `cargo` for runnables.
132 pub override_cargo: Option<String>,
133 /// Additional arguments for the `cargo`, e.g. `--release`.
134 pub cargo_extra_args: Vec<String>,
135}
136
127#[derive(Debug, Clone, Default)] 137#[derive(Debug, Clone, Default)]
128pub struct ClientCapsConfig { 138pub struct ClientCapsConfig {
129 pub location_link: bool, 139 pub location_link: bool,
@@ -164,6 +174,7 @@ impl Config {
164 extra_args: Vec::new(), 174 extra_args: Vec::new(),
165 features: Vec::new(), 175 features: Vec::new(),
166 }), 176 }),
177 runnables: RunnablesConfig::default(),
167 178
168 inlay_hints: InlayHintsConfig { 179 inlay_hints: InlayHintsConfig {
169 type_hints: true, 180 type_hints: true,
@@ -220,6 +231,10 @@ impl Config {
220 load_out_dirs_from_check: data.cargo_loadOutDirsFromCheck, 231 load_out_dirs_from_check: data.cargo_loadOutDirsFromCheck,
221 target: data.cargo_target.clone(), 232 target: data.cargo_target.clone(),
222 }; 233 };
234 self.runnables = RunnablesConfig {
235 override_cargo: data.runnables_overrideCargo,
236 cargo_extra_args: data.runnables_cargoExtraArgs,
237 };
223 238
224 self.proc_macro_srv = if data.procMacro_enable { 239 self.proc_macro_srv = if data.procMacro_enable {
225 std::env::current_exe().ok().map(|path| (path, vec!["proc-macro".into()])) 240 std::env::current_exe().ok().map(|path| (path, vec!["proc-macro".into()]))
@@ -474,6 +489,9 @@ config_data! {
474 notifications_cargoTomlNotFound: bool = true, 489 notifications_cargoTomlNotFound: bool = true,
475 procMacro_enable: bool = false, 490 procMacro_enable: bool = false,
476 491
492 runnables_overrideCargo: Option<String> = None,
493 runnables_cargoExtraArgs: Vec<String> = Vec::new(),
494
477 rustfmt_extraArgs: Vec<String> = Vec::new(), 495 rustfmt_extraArgs: Vec<String> = Vec::new(),
478 rustfmt_overrideCommand: Option<Vec<String>> = None, 496 rustfmt_overrideCommand: Option<Vec<String>> = None,
479 497
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index afcec63ad..e970abb7c 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -491,6 +491,7 @@ pub(crate) fn handle_runnables(
491 } 491 }
492 492
493 // Add `cargo check` and `cargo test` for all targets of the whole package 493 // Add `cargo check` and `cargo test` for all targets of the whole package
494 let config = &snap.config.runnables;
494 match cargo_spec { 495 match cargo_spec {
495 Some(spec) => { 496 Some(spec) => {
496 for &cmd in ["check", "test"].iter() { 497 for &cmd in ["check", "test"].iter() {
@@ -500,12 +501,14 @@ pub(crate) fn handle_runnables(
500 kind: lsp_ext::RunnableKind::Cargo, 501 kind: lsp_ext::RunnableKind::Cargo,
501 args: lsp_ext::CargoRunnable { 502 args: lsp_ext::CargoRunnable {
502 workspace_root: Some(spec.workspace_root.clone().into()), 503 workspace_root: Some(spec.workspace_root.clone().into()),
504 override_cargo: config.override_cargo.clone(),
503 cargo_args: vec![ 505 cargo_args: vec![
504 cmd.to_string(), 506 cmd.to_string(),
505 "--package".to_string(), 507 "--package".to_string(),
506 spec.package.clone(), 508 spec.package.clone(),
507 "--all-targets".to_string(), 509 "--all-targets".to_string(),
508 ], 510 ],
511 cargo_extra_args: config.cargo_extra_args.clone(),
509 executable_args: Vec::new(), 512 executable_args: Vec::new(),
510 expect_test: None, 513 expect_test: None,
511 }, 514 },
@@ -519,7 +522,9 @@ pub(crate) fn handle_runnables(
519 kind: lsp_ext::RunnableKind::Cargo, 522 kind: lsp_ext::RunnableKind::Cargo,
520 args: lsp_ext::CargoRunnable { 523 args: lsp_ext::CargoRunnable {
521 workspace_root: None, 524 workspace_root: None,
525 override_cargo: config.override_cargo.clone(),
522 cargo_args: vec!["check".to_string(), "--workspace".to_string()], 526 cargo_args: vec!["check".to_string(), "--workspace".to_string()],
527 cargo_extra_args: config.cargo_extra_args.clone(),
523 executable_args: Vec::new(), 528 executable_args: Vec::new(),
524 expect_test: None, 529 expect_test: None,
525 }, 530 },
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 43ff191da..fee0bb69c 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -171,10 +171,14 @@ pub enum RunnableKind {
171#[derive(Deserialize, Serialize, Debug)] 171#[derive(Deserialize, Serialize, Debug)]
172#[serde(rename_all = "camelCase")] 172#[serde(rename_all = "camelCase")]
173pub struct CargoRunnable { 173pub struct CargoRunnable {
174 // command to be executed instead of cargo
175 pub override_cargo: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none")] 176 #[serde(skip_serializing_if = "Option::is_none")]
175 pub workspace_root: Option<PathBuf>, 177 pub workspace_root: Option<PathBuf>,
176 // command, --package and --lib stuff 178 // command, --package and --lib stuff
177 pub cargo_args: Vec<String>, 179 pub cargo_args: Vec<String>,
180 // user-specified additional cargo args, like `--release`.
181 pub cargo_extra_args: Vec<String>,
178 // stuff after -- 182 // stuff after --
179 pub executable_args: Vec<String>, 183 pub executable_args: Vec<String>,
180 #[serde(skip_serializing_if = "Option::is_none")] 184 #[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 59e780b7d..aeacde0f7 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(
740 file_id: FileId, 740 file_id: FileId,
741 runnable: Runnable, 741 runnable: Runnable,
742) -> Result<lsp_ext::Runnable> { 742) -> Result<lsp_ext::Runnable> {
743 let config = &snap.config.runnables;
743 let spec = CargoTargetSpec::for_file(snap, file_id)?; 744 let spec = CargoTargetSpec::for_file(snap, file_id)?;
744 let workspace_root = spec.as_ref().map(|it| it.workspace_root.clone()); 745 let workspace_root = spec.as_ref().map(|it| it.workspace_root.clone());
745 let target = spec.as_ref().map(|s| s.target.clone()); 746 let target = spec.as_ref().map(|s| s.target.clone());
@@ -754,7 +755,9 @@ pub(crate) fn runnable(
754 kind: lsp_ext::RunnableKind::Cargo, 755 kind: lsp_ext::RunnableKind::Cargo,
755 args: lsp_ext::CargoRunnable { 756 args: lsp_ext::CargoRunnable {
756 workspace_root: workspace_root.map(|it| it.into()), 757 workspace_root: workspace_root.map(|it| it.into()),
758 override_cargo: config.override_cargo.clone(),
757 cargo_args, 759 cargo_args,
760 cargo_extra_args: config.cargo_extra_args.clone(),
758 executable_args, 761 executable_args,
759 expect_test: None, 762 expect_test: None,
760 }, 763 },
diff --git a/crates/rust-analyzer/tests/rust-analyzer/main.rs b/crates/rust-analyzer/tests/rust-analyzer/main.rs
index 06726f957..e51eb2626 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() {}
107 "args": { 107 "args": {
108 "cargoArgs": ["test", "--package", "foo", "--test", "spam"], 108 "cargoArgs": ["test", "--package", "foo", "--test", "spam"],
109 "executableArgs": ["test_eggs", "--exact", "--nocapture"], 109 "executableArgs": ["test_eggs", "--exact", "--nocapture"],
110 "cargoExtraArgs": [],
111 "overrideCargo": null,
110 "workspaceRoot": server.path().join("foo") 112 "workspaceRoot": server.path().join("foo")
111 }, 113 },
112 "kind": "cargo", 114 "kind": "cargo",
@@ -127,6 +129,8 @@ fn main() {}
127 "args": { 129 "args": {
128 "cargoArgs": ["check", "--package", "foo", "--all-targets"], 130 "cargoArgs": ["check", "--package", "foo", "--all-targets"],
129 "executableArgs": [], 131 "executableArgs": [],
132 "cargoExtraArgs": [],
133 "overrideCargo": null,
130 "workspaceRoot": server.path().join("foo") 134 "workspaceRoot": server.path().join("foo")
131 }, 135 },
132 "kind": "cargo", 136 "kind": "cargo",
@@ -136,6 +140,8 @@ fn main() {}
136 "args": { 140 "args": {
137 "cargoArgs": ["test", "--package", "foo", "--all-targets"], 141 "cargoArgs": ["test", "--package", "foo", "--all-targets"],
138 "executableArgs": [], 142 "executableArgs": [],
143 "cargoExtraArgs": [],
144 "overrideCargo": null,
139 "workspaceRoot": server.path().join("foo") 145 "workspaceRoot": server.path().join("foo")
140 }, 146 },
141 "kind": "cargo", 147 "kind": "cargo",
diff --git a/editors/code/package.json b/editors/code/package.json
index bdd8a0c29..cc2ac3bd2 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -651,6 +651,22 @@
651 ], 651 ],
652 "default": "full", 652 "default": "full",
653 "description": "The strategy to use when inserting new imports or merging imports." 653 "description": "The strategy to use when inserting new imports or merging imports."
654 },
655 "rust-analyzer.runnables.overrideCargo": {
656 "type": [
657 "null",
658 "string"
659 ],
660 "default": null,
661 "description": "Command to be executed instead of 'cargo' for runnables."
662 },
663 "rust-analyzer.runnables.cargoExtraArgs": {
664 "type": "array",
665 "items": {
666 "type": "string"
667 },
668 "default": [],
669 "description": "Additional arguments to be passed to cargo for runnables such as tests or binaries.\nFor example, it may be '--release'"
654 } 670 }
655 } 671 }
656 }, 672 },
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index eb422d3e7..f286b68a6 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -69,8 +69,10 @@ export interface Runnable {
69 args: { 69 args: {
70 workspaceRoot?: string; 70 workspaceRoot?: string;
71 cargoArgs: string[]; 71 cargoArgs: string[];
72 cargoExtraArgs: string[];
72 executableArgs: string[]; 73 executableArgs: string[];
73 expectTest?: boolean; 74 expectTest?: boolean;
75 overrideCargo?: string;
74 }; 76 };
75} 77}
76export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("experimental/runnables"); 78export 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 de68f27ae..459b7f250 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
129 } 129 }
130 130
131 const args = [...runnable.args.cargoArgs]; // should be a copy! 131 const args = [...runnable.args.cargoArgs]; // should be a copy!
132 args.push(...runnable.args.cargoExtraArgs); // Append user-specified cargo options.
132 if (runnable.args.executableArgs.length > 0) { 133 if (runnable.args.executableArgs.length > 0) {
133 args.push('--', ...runnable.args.executableArgs); 134 args.push('--', ...runnable.args.executableArgs);
134 } 135 }
@@ -139,6 +140,7 @@ export async function createTask(runnable: ra.Runnable, config: Config): Promise
139 args: args.slice(1), 140 args: args.slice(1),
140 cwd: runnable.args.workspaceRoot || ".", 141 cwd: runnable.args.workspaceRoot || ".",
141 env: prepareEnv(runnable, config.runnableEnv), 142 env: prepareEnv(runnable, config.runnableEnv),
143 overrideCargo: runnable.args.overrideCargo,
142 }; 144 };
143 145
144 const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate() 146 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 14abbd5b7..a3ff15102 100644
--- a/editors/code/src/tasks.ts
+++ b/editors/code/src/tasks.ts
@@ -13,6 +13,7 @@ export interface CargoTaskDefinition extends vscode.TaskDefinition {
13 args?: string[]; 13 args?: string[];
14 cwd?: string; 14 cwd?: string;
15 env?: { [key: string]: string }; 15 env?: { [key: string]: string };
16 overrideCargo?: string;
16} 17}
17 18
18class CargoTaskProvider implements vscode.TaskProvider { 19class CargoTaskProvider implements vscode.TaskProvider {
@@ -98,7 +99,14 @@ export async function buildCargoTask(
98 } 99 }
99 100
100 if (!exec) { 101 if (!exec) {
101 exec = new vscode.ShellExecution(toolchain.cargoPath(), args, definition); 102 // Check whether we must use a user-defined substitute for cargo.
103 const cargoCommand = definition.overrideCargo ? definition.overrideCargo : toolchain.cargoPath();
104
105 // Prepare the whole command as one line. It is required if user has provided override command which contains spaces,
106 // for example "wrapper cargo". Without manual preparation the overridden command will be quoted and fail to execute.
107 const fullCommand = [cargoCommand, ...args].join(" ");
108
109 exec = new vscode.ShellExecution(fullCommand, definition);
102 } 110 }
103 111
104 return new vscode.Task( 112 return new vscode.Task(
diff --git a/editors/code/tests/unit/runnable_env.test.ts b/editors/code/tests/unit/runnable_env.test.ts
index f2f53e91a..c5600cf64 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 {
9 kind: "cargo", 9 kind: "cargo",
10 args: { 10 args: {
11 cargoArgs: [], 11 cargoArgs: [],
12 executableArgs: [] 12 executableArgs: [],
13 cargoExtraArgs: []
13 } 14 }
14 }; 15 };
15} 16}