Repository: rust-analyzer/rust-analyzer
Pull request #5988 (2020-09-12 15:15:22)

Postfix completions for fmt-like string literals
This pull request adds a bunch of new postfix completions for `format`-like string literls.

For example, `"{32} {some_var:?}".println` will expand to `println!("{} {:?}", 32, some_var)`.

Postfix completions were added for most common format-like macros:

- `println` -> `println!(...)`
- `fmt` -> `format!(...)`
- `panic` -> `panic!(...)`
- `log` macros:
  + `logi` -> `log::info!(...)`
  + `logw` -> `log::warn!(...)`
  + `loge` -> `log::error!(...)`
  + `logt` -> `log::trace!(...)`
  + `logd` -> `log::debug!(...)`

![fmt_postfix](https://user-images.githubusercontent.com/12111581/92998650-a048af80-f523-11ea-8fd8-410146de8caa.gif)




Patch:
diff --git a/crates/assists/src/utils/insert_use.rs b/crates/assists/src/utils/insert_use.rs
index f6025c99a1..8dd4fe6074 100644
--- a/crates/assists/src/utils/insert_use.rs
+++ b/crates/assists/src/utils/insert_use.rs
@@ -4,14 +4,13 @@ use std::{
     iter::{self, successors},
 };
 
-use itertools::{EitherOrBoth, Itertools};
+use ast::{
+    edit::{AstNodeEdit, IndentLevel},
+    PathSegmentKind, VisibilityOwner,
+};
 use syntax::{
     algo,
-    ast::{
-        self,
-        edit::{AstNodeEdit, IndentLevel},
-        make, AstNode, PathSegmentKind, VisibilityOwner,
-    },
+    ast::{self, make, AstNode},
     InsertPosition, SyntaxElement, SyntaxNode,
 };
 
@@ -194,13 +193,13 @@ fn recursive_merge(
             false => None,
         })
         .collect::<Option<Vec<_>>>()?;
-    use_trees.sort_unstable_by(|a, b| path_cmp_for_sort(a.path(), b.path()));
+    use_trees.sort_unstable_by(|a, b| path_cmp_opt(a.path(), b.path()));
     for rhs_t in rhs.use_tree_list().into_iter().flat_map(|list| list.use_trees()) {
         if !merge.is_tree_allowed(&rhs_t) {
             return None;
         }
         let rhs_path = rhs_t.path();
-        match use_trees.binary_search_by(|p| path_cmp_bin_search(p.path(), rhs_path.clone())) {
+        match use_trees.binary_search_by(|p| path_cmp_opt(p.path(), rhs_path.clone())) {
             Ok(idx) => {
                 let lhs_t = &mut use_trees[idx];
                 let lhs_path = lhs_t.path()?;
@@ -308,77 +307,39 @@ fn path_len(path: ast::Path) -> usize {
 
 /// Orders paths in the following way:
 /// the sole self token comes first, after that come uppercase identifiers, then lowercase identifiers
-// FIXME: rustfmt sorts lowercase idents before uppercase, in general we want to have the same ordering rustfmt has
+// FIXME: rustfmt sort lowercase idents before uppercase, in general we want to have the same ordering rustfmt has
 // which is `self` and `super` first, then identifier imports with lowercase ones first, then glob imports and at last list imports.
 // Example foo::{self, foo, baz, Baz, Qux, *, {Bar}}
-fn path_cmp_for_sort(a: Option<ast::Path>, b: Option<ast::Path>) -> Ordering {
-    match (a, b) {
-        (None, None) => Ordering::Equal,
-        (None, Some(_)) => Ordering::Less,
-        (Some(_), None) => Ordering::Greater,
-        (Some(ref a), Some(ref b)) => match (path_is_self(a), path_is_self(b)) {
-            (true, true) => Ordering::Equal,
-            (true, false) => Ordering::Less,
-            (false, true) => Ordering::Greater,
-            (false, false) => path_cmp_short(a, b),
-        },
+fn path_cmp(a: &ast::Path, b: &ast::Path) -> Ordering {
+    match (path_is_self(a), path_is_self(b)) {
+        (true, true) => Ordering::Equal,
+        (true, false) => Ordering::Less,
+        (false, true) => Ordering::Greater,
+        (false, false) => {
+            let a = segment_iter(a);
+            let b = segment_iter(b);
+            // cmp_by would be useful for us here but that is currently unstable
+            // cmp doesnt work due the lifetimes on text's return type
+            a.zip(b)
+                .flat_map(|(seg, seg2)| seg.name_ref().zip(seg2.name_ref()))
+                .find_map(|(a, b)| match a.text().cmp(b.text()) {
+                    ord @ Ordering::Greater | ord @ Ordering::Less => Some(ord),
+                    Ordering::Equal => None,
+                })
+                .unwrap_or(Ordering::Equal)
+        }
     }
 }
 
-/// Path comparison func for binary searching for merging.
-fn path_cmp_bin_search(lhs: Option<ast::Path>, rhs: Option<ast::Path>) -> Ordering {
-    match (lhs, rhs) {
+fn path_cmp_opt(a: Option<ast::Path>, b: Option<ast::Path>) -> Ordering {
+    match (a, b) {
         (None, None) => Ordering::Equal,
         (None, Some(_)) => Ordering::Less,
         (Some(_), None) => Ordering::Greater,
-        (Some(ref a), Some(ref b)) => path_cmp_short(a, b),
+        (Some(a), Some(b)) => path_cmp(&a, &b),
     }
 }
 
-/// Short circuiting comparison, if both paths are equal until one of them ends they are considered
-/// equal
-fn path_cmp_short(a: &ast::Path, b: &ast::Path) -> Ordering {
-    let a = segment_iter(a);
-    let b = segment_iter(b);
-    // cmp_by would be useful for us here but that is currently unstable
-    // cmp doesnt work due the lifetimes on text's return type
-    a.zip(b)
-        .find_map(|(a, b)| match path_segment_cmp(&a, &b) {
-            Ordering::Equal => None,
-            ord => Some(ord),
-        })
-        .unwrap_or(Ordering::Equal)
-}
-
-/// Compares to paths, if one ends earlier than the other the has_tl parameters decide which is
-/// greater as a a path that has a tree list should be greater, while one that just ends without
-/// a tree list should be considered less.
-fn use_tree_path_cmp(a: &ast::Path, a_has_tl: bool, b: &ast::Path, b_has_tl: bool) -> Ordering {
-    let a_segments = segment_iter(a);
-    let b_segments = segment_iter(b);
-    // cmp_by would be useful for us here but that is currently unstable
-    // cmp doesnt work due the lifetimes on text's return type
-    a_segments
-        .zip_longest(b_segments)
-        .find_map(|zipped| match zipped {
-            EitherOrBoth::Both(ref a, ref b) => match path_segment_cmp(a, b) {
-                Ordering::Equal => None,
-                ord => Some(ord),
-            },
-            EitherOrBoth::Left(_) if !b_has_tl => Some(Ordering::Greater),
-            EitherOrBoth::Left(_) => Some(Ordering::Less),
-            EitherOrBoth::Right(_) if !a_has_tl => Some(Ordering::Less),
-            EitherOrBoth::Right(_) => Some(Ordering::Greater),
-        })
-        .unwrap_or(Ordering::Equal)
-}
-
-fn path_segment_cmp(a: &ast::PathSegment, b: &ast::PathSegment) -> Ordering {
-    let a = a.name_ref();
-    let b = b.name_ref();
-    a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text))
-}
-
 /// What type of merges are allowed.
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum MergeBehaviour {
@@ -428,6 +389,7 @@ impl ImportGroup {
             PathSegmentKind::Name(name) => match name.text().as_str() {
                 "std" => ImportGroup::Std,
                 "core" => ImportGroup::Std,
+                // FIXME: can be ThisModule as well
                 _ => ImportGroup::ExternCrate,
             },
             PathSegmentKind::Type { .. } => unreachable!(),
@@ -453,30 +415,30 @@ fn find_insert_position(
         .as_syntax_node()
         .children()
         .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
-        .flat_map(|(use_, node)| {
-            let tree = use_.use_tree()?;
-            let path = tree.path()?;
-            let has_tl = tree.use_tree_list().is_some();
-            Some((path, has_tl, node))
-        });
+        .flat_map(|(use_, node)| use_.use_tree().and_then(|tree| tree.path()).zip(Some(node)));
     // Iterator that discards anything thats not in the required grouping
     // This implementation allows the user to rearrange their import groups as this only takes the first group that fits
     let group_iter = path_node_iter
         .clone()
-        .skip_while(|(path, ..)| ImportGroup::new(path) != group)
-        .take_while(|(path, ..)| ImportGroup::new(path) == group);
+        .skip_while(|(path, _)| ImportGroup::new(path) != group)
+        .take_while(|(path, _)| ImportGroup::new(path) == group);
 
+    let segments = segment_iter(&insert_path);
     // 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
     let mut last = None;
     // find the element that would come directly after our new import
-    let post_insert = group_iter.inspect(|(.., node)| last = Some(node.clone())).find(
-        |&(ref path, has_tl, _)| {
-            use_tree_path_cmp(&insert_path, false, path, has_tl) != Ordering::Greater
-        },
-    );
+    let post_insert =
+        group_iter.inspect(|(_, node)| last = Some(node.clone())).find(|(path, _)| {
+            let check_segments = segment_iter(&path);
+            segments
+                .clone()
+                .zip(check_segments)
+                .flat_map(|(seg, seg2)| seg.name_ref().zip(seg2.name_ref()))
+                .all(|(l, r)| l.text() <= r.text())
+        });
     match post_insert {
         // insert our import before that element
-        Some((.., node)) => (InsertPosition::Before(node.into()), AddBlankLine::After),
+        Some((_, node)) => (InsertPosition::Before(node.into()), AddBlankLine::After),
         // there is no element after our new import, so append it to the end of the group
         None => match last {
             Some(node) => (InsertPosition::After(node.into()), AddBlankLine::Before),
@@ -486,10 +448,10 @@ fn find_insert_position(
                 let mut last = None;
                 // find the group that comes after where we want to insert
                 let post_group = path_node_iter
-                    .inspect(|(.., node)| last = Some(node.clone()))
-                    .find(|(p, ..)| ImportGroup::new(p) > group);
+                    .inspect(|(_, node)| last = Some(node.clone()))
+                    .find(|(p, _)| ImportGroup::new(p) > group);
                 match post_group {
-                    Some((.., node)) => {
+                    Some((_, node)) => {
                         (InsertPosition::Before(node.into()), AddBlankLine::AfterTwice)
                     }
                     // there is no such group, so append after the last one
@@ -882,6 +844,7 @@ use foo::bar::baz::Qux;",
     }
 
     #[test]
+    #[ignore] // FIXME: Order changes when switching lhs and rhs
     fn skip_merge_last_too_long2() {
         check_last(
             "foo::bar::baz::Qux",
diff --git a/crates/ide/src/completion/complete_postfix.rs b/crates/ide/src/completion/complete_postfix.rs
index 26a5af5b9a..db5319618e 100644
--- a/crates/ide/src/completion/complete_postfix.rs
+++ b/crates/ide/src/completion/complete_postfix.rs
@@ -1,11 +1,15 @@
 //! FIXME: write short doc here
+
+mod format_like;
+
 use assists::utils::TryEnum;
 use syntax::{
-    ast::{self, AstNode},
+    ast::{self, AstNode, AstToken},
     TextRange, TextSize,
 };
 use text_edit::TextEdit;
 
+use self::format_like::add_format_like_completions;
 use crate::{
     completion::{
         completion_config::SnippetCap,
@@ -207,6 +211,12 @@ pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
         &format!("${{1}}({})", receiver_text),
     )
     .add_to(acc);
+
+    if let ast::Expr::Literal(literal) = dot_receiver.clone() {
+        if let Some(literal_text) = ast::String::cast(literal.token()) {
+            add_format_like_completions(acc, ctx, &dot_receiver, cap, &literal_text);
+        }
+    }
 }
 
 fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
@@ -392,4 +402,53 @@ fn main() {
         check_edit("dbg", r#"fn main() { &&42.<|> }"#, r#"fn main() { dbg!(&&42) }"#);
         check_edit("refm", r#"fn main() { &&42.<|> }"#, r#"fn main() { &&&mut 42 }"#);
     }
+
+    #[test]
+    fn postfix_completion_for_format_like_strings() {
+        check_edit(
+            "fmt",
+            r#"fn main() { "{some_var:?}".<|> }"#,
+            r#"fn main() { format!("{:?}", some_var) }"#,
+        );
+        check_edit(
+            "panic",
+            r#"fn main() { "Panic with {a}".<|> }"#,
+            r#"fn main() { panic!("Panic with {}", a) }"#,
+        );
+        check_edit(
+            "println",
+            r#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".<|> }"#,
+            r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }) }"#,
+        );
+        check_edit(
+            "loge",
+            r#"fn main() { "{2+2}".<|> }"#,
+            r#"fn main() { log::error!("{}", 2+2) }"#,
+        );
+        check_edit(
+            "logt",
+            r#"fn main() { "{2+2}".<|> }"#,
+            r#"fn main() { log::trace!("{}", 2+2) }"#,
+        );
+        check_edit(
+            "logd",
+            r#"fn main() { "{2+2}".<|> }"#,
+            r#"fn main() { log::debug!("{}", 2+2) }"#,
+        );
+        check_edit(
+            "logi",
+            r#"fn main() { "{2+2}".<|> }"#,
+            r#"fn main() { log::info!("{}", 2+2) }"#,
+        );
+        check_edit(
+            "logw",
+            r#"fn main() { "{2+2}".<|> }"#,
+            r#"fn main() { log::warn!("{}", 2+2) }"#,
+        );
+        check_edit(
+            "loge",
+            r#"fn main() { "{2+2}".<|> }"#,
+            r#"fn main() { log::error!("{}", 2+2) }"#,
+        );
+    }
 }
diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs
new file mode 100644
index 0000000000..0287fc8030
--- /dev/null
+++ b/crates/ide/src/completion/complete_postfix/format_like.rs
@@ -0,0 +1,277 @@
+// Feature: Postfix completion for `format`-like strings.
+//
+// `"Result {result} is {2 + 2}"` is expanded to the `"Result {} is {}", result, 2 + 2`.
+//
+// The following postfix snippets are available:
+//
+// - `format` -> `format!(...)`
+// - `panic` -> `panic!(...)`
+// - `println` -> `println!(...)`
+// - `log`:
+//   + `logd` -> `log::debug!(...)`
+//   + `logt` -> `log::trace!(...)`
+//   + `logi` -> `log::info!(...)`
+//   + `logw` -> `log::warn!(...)`
+//   + `loge` -> `log::error!(...)`
+
+use crate::completion::{
+    complete_postfix::postfix_snippet, completion_config::SnippetCap,
+    completion_context::CompletionContext, completion_item::Completions,
+};
+use syntax::ast::{self, AstToken};
+
+/// Mapping ("postfix completion item" => "macro to use")
+static KINDS: &[(&str, &str)] = &[
+    ("fmt", "format!"),
+    ("panic", "panic!"),
+    ("println", "println!"),
+    ("logd", "log::debug!"),
+    ("logt", "log::trace!"),
+    ("logi", "log::info!"),
+    ("logw", "log::warn!"),
+    ("loge", "log::error!"),
+];
+
+pub(super) fn add_format_like_completions(
+    acc: &mut Completions,
+    ctx: &CompletionContext,
+    dot_receiver: &ast::Expr,
+    cap: SnippetCap,
+    receiver_text: &ast::String,
+) {
+    let input = match string_literal_contents(receiver_text) {
+        // It's not a string literal, do not parse input.
+        Some(input) => input,
+        None => return,
+    };
+
+    let mut parser = FormatStrParser::new(input);
+
+    if parser.parse().is_ok() {
+        for (label, macro_name) in KINDS {
+            let snippet = parser.into_suggestion(macro_name);
+
+            postfix_snippet(ctx, cap, &dot_receiver, label, macro_name, &snippet).add_to(acc);
+        }
+    }
+}
+
+/// Checks whether provided item is a string literal.
+fn string_literal_contents(item: &ast::String) -> Option<String> {
+    let item = item.text();
+    if item.len() >= 2 && item.starts_with("\"") && item.ends_with("\"") {
+        return Some(item[1..item.len() - 1].to_owned());
+    }
+
+    None
+}
+
+/// Parser for a format-like string. It is more allowing in terms of string contents,
+/// as we expect variable placeholders to be filled with expressions.
+#[derive(Debug)]
+pub struct FormatStrParser {
+    input: String,
+    output: String,
+    extracted_expressions: Vec<String>,
+    state: State,
+    parsed: bool,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+enum State {
+    NotExpr,
+    MaybeExpr,
+    Expr,
+    MaybeIncorrect,
+    FormatOpts,
+}
+
+impl FormatStrParser {
+    pub fn new(input: String) -> Self {
+        Self {
+            input: input.into(),
+            output: String::new(),
+            extracted_expressions: Vec::new(),
+            state: State::NotExpr,
+            parsed: false,
+        }
+    }
+
+    pub fn parse(&mut self) -> Result<(), ()> {
+        let mut current_expr = String::new();
+
+        let mut placeholder_id = 1;
+
+        // Count of open braces inside of an expression.
+        // We assume that user knows what they're doing, thus we treat it like a correct pattern, e.g.
+        // "{MyStruct { val_a: 0, val_b: 1 }}".
+        let mut inexpr_open_count = 0;
+
+        for chr in self.input.chars() {
+            match (self.state, chr) {
+                (State::NotExpr, '{') => {
+                    self.output.push(chr);
+                    self.state = State::MaybeExpr;
+                }
+                (State::NotExpr, '}') => {
+                    self.output.push(chr);
+                    self.state = State::MaybeIncorrect;
+                }
+                (State::NotExpr, _) => {
+                    self.output.push(chr);
+                }
+                (State::MaybeIncorrect, '}') => {
+                    // It's okay, we met "}}".
+                    self.output.push(chr);
+                    self.state = State::NotExpr;
+                }
+                (State::MaybeIncorrect, _) => {
+                    // Error in the string.
+                    return Err(());
+                }
+                (State::MaybeExpr, '{') => {
+                    self.output.push(chr);
+                    self.state = State::NotExpr;
+                }
+                (State::MaybeExpr, '}') => {
+                    // This is an empty sequence '{}'. Replace it with placeholder.
+                    self.output.push(chr);
+                    self.extracted_expressions.push(format!("${}", placeholder_id));
+                    placeholder_id += 1;
+                    self.state = State::NotExpr;
+                }
+                (State::MaybeExpr, _) => {
+                    current_expr.push(chr);
+                    self.state = State::Expr;
+                }
+                (State::Expr, '}') => {
+                    if inexpr_open_count == 0 {
+                        self.output.push(chr);
+                        self.extracted_expressions.push(current_expr.trim().into());
+                        current_expr = String::new();
+                        self.state = State::NotExpr;
+                    } else {
+                        // We're closing one brace met before inside of the expression.
+                        current_expr.push(chr);
+                        inexpr_open_count -= 1;
+                    }
+                }
+                (State::Expr, ':') => {
+                    if inexpr_open_count == 0 {
+                        // We're outside of braces, thus assume that it's a specifier, like "{Some(value):?}"
+                        self.output.push(chr);
+                        self.extracted_expressions.push(current_expr.trim().into());
+                        current_expr = String::new();
+                        self.state = State::FormatOpts;
+                    } else {
+                        // We're inside of braced expression, assume that it's a struct field name/value delimeter.
+                        current_expr.push(chr);
+                    }
+                }
+                (State::Expr, '{') => {
+                    current_expr.push(chr);
+                    inexpr_open_count += 1;
+                }
+                (State::Expr, _) => {
+                    current_expr.push(chr);
+                }
+                (State::FormatOpts, '}') => {
+                    self.output.push(chr);
+                    self.state = State::NotExpr;
+                }
+                (State::FormatOpts, _) => {
+                    self.output.push(chr);
+                }
+            }
+        }
+
+        if self.state != State::NotExpr {
+            return Err(());
+        }
+
+        self.parsed = true;
+        Ok(())
+    }
+
+    pub fn into_suggestion(&self, macro_name: &str) -> String {
+        assert!(self.parsed, "Attempt to get a suggestion from not parsed expression");
+
+        let expressions_as_string = self.extracted_expressions.join(", ");
+        format!(r#"{}("{}", {})"#, macro_name, self.output, expressions_as_string)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use expect_test::{expect, Expect};
+
+    fn check(input: &str, expect: &Expect) {
+        let mut parser = FormatStrParser::new((*input).to_owned());
+        let outcome_repr = if parser.parse().is_ok() {
+            // Parsing should be OK, expected repr is "string; expr_1, expr_2".
+            if parser.extracted_expressions.is_empty() {
+                parser.output
+            } else {
+                format!("{}; {}", parser.output, parser.extracted_expressions.join(", "))
+            }
+        } else {
+            // Parsing should fail, expected repr is "-".
+            "-".to_owned()
+        };
+
+        expect.assert_eq(&outcome_repr);
+    }
+
+    #[test]
+    fn format_str_parser() {
+        let test_vector = &[
+            ("no expressions", expect![["no expressions"]]),
+            ("{expr} is {2 + 2}", expect![["{} is {}; expr, 2 + 2"]]),
+            ("{expr:?}", expect![["{:?}; expr"]]),
+            ("{malformed", expect![["-"]]),
+            ("malformed}", expect![["-"]]),
+            ("{{correct", expect![["{{correct"]]),
+            ("correct}}", expect![["correct}}"]]),
+            ("{correct}}}", expect![["{}}}; correct"]]),
+            ("{correct}}}}}", expect![["{}}}}}; correct"]]),
+            ("{incorrect}}", expect![["-"]]),
+            ("placeholders {} {}", expect![["placeholders {} {}; $1, $2"]]),
+            ("mixed {} {2 + 2} {}", expect![["mixed {} {} {}; $1, 2 + 2, $2"]]),
+            (
+                "{SomeStruct { val_a: 0, val_b: 1 }}",
+                expect![["{}; SomeStruct { val_a: 0, val_b: 1 }"]],
+            ),
+            ("{expr:?} is {2.32f64:.5}", expect![["{:?} is {:.5}; expr, 2.32f64"]]),
+            (
+                "{SomeStruct { val_a: 0, val_b: 1 }:?}",
+                expect![["{:?}; SomeStruct { val_a: 0, val_b: 1 }"]],
+            ),
+            ("{     2 + 2        }", expect![["{}; 2 + 2"]]),
+        ];
+
+        for (input, output) in test_vector {
+            check(input, output)
+        }
+    }
+
+    #[test]
+    fn test_into_suggestion() {
+        let test_vector = &[
+            ("println!", "{}", r#"println!("{}", $1)"#),
+            (
+                "log::info!",
+                "{} {expr} {} {2 + 2}",
+                r#"log::info!("{} {} {} {}", $1, expr, $2, 2 + 2)"#,
+            ),
+            ("format!", "{expr:?}", r#"format!("{:?}", expr)"#),
+        ];
+
+        for (kind, input, output) in test_vector {
+            let mut parser = FormatStrParser::new((*input).to_owned());
+            parser.parse().expect("Parsing must succeed");
+
+            assert_eq!(&parser.into_suggestion(*kind), output);
+        }
+    }
+}
diff --git a/crates/ide/src/completion/completion_context.rs b/crates/ide/src/completion/completion_context.rs
index 671b13328c..101be8eb54 100644
--- a/crates/ide/src/completion/completion_context.rs
+++ b/crates/ide/src/completion/completion_context.rs
@@ -469,7 +469,7 @@ impl<'a> CompletionContext<'a> {
                     }
                 } else {
                     false
-                }
+                };
         }
         if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) {
             // As above
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 0ab4c37bff..42e1ad376c 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -38,7 +38,6 @@ pub struct Config {
     pub cargo: CargoConfig,
     pub rustfmt: RustfmtConfig,
     pub flycheck: Option<FlycheckConfig>,
-    pub runnables: RunnablesConfig,
 
     pub inlay_hints: InlayHintsConfig,
     pub completion: CompletionConfig,
@@ -125,15 +124,6 @@ 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,
@@ -174,7 +164,6 @@ impl Config {
                 extra_args: Vec::new(),
                 features: Vec::new(),
             }),
-            runnables: RunnablesConfig::default(),
 
             inlay_hints: InlayHintsConfig {
                 type_hints: true,
@@ -231,10 +220,6 @@ 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()]))
@@ -489,9 +474,6 @@ 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 e970abb7c0..afcec63ad1 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -491,7 +491,6 @@ 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() {
@@ -501,14 +500,12 @@ 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,
                     },
@@ -522,9 +519,7 @@ 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 fee0bb69c7..43ff191dab 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -171,14 +171,10 @@ 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 aeacde0f7a..59e780b7d2 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -740,7 +740,6 @@ 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());
@@ -755,9 +754,7 @@ 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 cc2ac3bd26..bdd8a0c298 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -651,22 +651,6 @@
                     ],
                     "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 f286b68a68..eb422d3e75 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -69,10 +69,8 @@ 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 459b7f250d..de68f27aec 100644
--- a/editors/code/src/run.ts
+++ b/editors/code/src/run.ts
@@ -129,7 +129,6 @@ 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);
     }
@@ -140,7 +139,6 @@ 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 a3ff151025..14abbd5b79 100644
--- a/editors/code/src/tasks.ts
+++ b/editors/code/src/tasks.ts
@@ -13,7 +13,6 @@ export interface CargoTaskDefinition extends vscode.TaskDefinition {
     args?: string[];
     cwd?: string;
     env?: { [key: string]: string };
-    overrideCargo?: string;
 }
 
 class CargoTaskProvider implements vscode.TaskProvider {
@@ -99,14 +98,7 @@ export async function buildCargoTask(
     }
 
     if (!exec) {
-        // 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);
+        exec = new vscode.ShellExecution(toolchain.cargoPath(), args, 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 e51eb26261..06726f9576 100644
--- a/crates/rust-analyzer/tests/rust-analyzer/main.rs
+++ b/crates/rust-analyzer/tests/rust-analyzer/main.rs
@@ -107,8 +107,6 @@ fn main() {}
             "args": {
               "cargoArgs": ["test", "--package", "foo", "--test", "spam"],
               "executableArgs": ["test_eggs", "--exact", "--nocapture"],
-              "cargoExtraArgs": [],
-              "overrideCargo": null,
               "workspaceRoot": server.path().join("foo")
             },
             "kind": "cargo",
@@ -129,8 +127,6 @@ fn main() {}
             "args": {
               "cargoArgs": ["check", "--package", "foo", "--all-targets"],
               "executableArgs": [],
-              "cargoExtraArgs": [],
-              "overrideCargo": null,
               "workspaceRoot": server.path().join("foo")
             },
             "kind": "cargo",
@@ -140,8 +136,6 @@ 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 c5600cf64f..f2f53e91ad 100644
--- a/editors/code/tests/unit/runnable_env.test.ts
+++ b/editors/code/tests/unit/runnable_env.test.ts
@@ -9,8 +9,7 @@ function makeRunnable(label: string): ra.Runnable {
         kind: "cargo",
         args: {
             cargoArgs: [],
-            executableArgs: [],
-            cargoExtraArgs: []
+            executableArgs: []
         }
     };
 }