]> git.lizzy.rs Git - rust.git/blobdiff - crates/ra_assists/src/introduce_variable.rs
ra_assists: assist "providers" can produce multiple assists
[rust.git] / crates / ra_assists / src / introduce_variable.rs
index c937a816cdaecd2c25c95c43bc59c8009bc30ecd..f0e012105f5ecb5008756fac6397eb208f631dfc 100644 (file)
@@ -8,7 +8,7 @@
 
 use crate::{AssistCtx, Assist};
 
-pub(crate) fn introduce_variable<'a>(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
+pub(crate) fn introduce_variable(mut ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
     let node = ctx.covering_node();
     if !valid_covering_node(node) {
         return None;
@@ -19,7 +19,7 @@ pub(crate) fn introduce_variable<'a>(ctx: AssistCtx<impl HirDatabase>) -> Option
     if indent.kind() != WHITESPACE {
         return None;
     }
-    ctx.build("introduce variable", move |edit| {
+    ctx.add_action("introduce variable", move |edit| {
         let mut buf = String::new();
 
         let cursor_offset = if wrap_in_block {
@@ -44,7 +44,23 @@ pub(crate) fn introduce_variable<'a>(ctx: AssistCtx<impl HirDatabase>) -> Option
             edit.replace(expr.syntax().range(), buf);
         } else {
             buf.push_str(";");
-            indent.text().push_to(&mut buf);
+
+            // We want to maintain the indent level,
+            // but we do not want to duplicate possible
+            // extra newlines in the indent block
+            for chunk in indent.text().chunks() {
+                if chunk.starts_with("\r\n") {
+                    buf.push_str("\r\n");
+                    buf.push_str(chunk.trim_start_matches("\r\n"));
+                } else if chunk.starts_with("\n") {
+                    buf.push_str("\n");
+                    buf.push_str(chunk.trim_start_matches("\n"));
+                } else {
+                    buf.push_str(chunk);
+                }
+            }
+
+            edit.target(expr.syntax().range());
             edit.replace(expr.syntax().range(), "var_name".to_string());
             edit.insert(anchor_stmt.range().start(), buf);
             if wrap_in_block {
@@ -52,40 +68,38 @@ pub(crate) fn introduce_variable<'a>(ctx: AssistCtx<impl HirDatabase>) -> Option
             }
         }
         edit.set_cursor(anchor_stmt.range().start() + cursor_offset);
-    })
+    });
+
+    ctx.build()
 }
 
 fn valid_covering_node(node: &SyntaxNode) -> bool {
     node.kind() != COMMENT
 }
-/// Check wether the node is a valid expression which can be extracted to a variable.
+/// Check whether the node is a valid expression which can be extracted to a variable.
 /// In general that's true for any expression, but in some cases that would produce invalid code.
 fn valid_target_expr(node: &SyntaxNode) -> Option<&ast::Expr> {
-    return match node.kind() {
+    match node.kind() {
         PATH_EXPR => None,
         BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()),
         RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()),
         LOOP_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()),
         _ => ast::Expr::cast(node),
-    };
+    }
 }
 
 /// Returns the syntax node which will follow the freshly introduced var
 /// and a boolean indicating whether we have to wrap it within a { } block
 /// to produce correct code.
 /// It can be a statement, the last in a block expression or a wanna be block
-/// expression like a lamba or match arm.
+/// expression like a lambda or match arm.
 fn anchor_stmt(expr: &ast::Expr) -> Option<(&SyntaxNode, bool)> {
     expr.syntax().ancestors().find_map(|node| {
         if ast::Stmt::cast(node).is_some() {
             return Some((node, false));
         }
 
-        if let Some(expr) = node
-            .parent()
-            .and_then(ast::Block::cast)
-            .and_then(|it| it.expr())
-        {
+        if let Some(expr) = node.parent().and_then(ast::Block::cast).and_then(|it| it.expr()) {
             if expr.syntax() == node {
                 return Some((node, false));
             }
@@ -104,7 +118,7 @@ fn anchor_stmt(expr: &ast::Expr) -> Option<(&SyntaxNode, bool)> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_range};
+    use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_range, check_assist_target, check_assist_range_target};
 
     #[test]
     fn test_introduce_var_simple() {
@@ -340,6 +354,70 @@ fn foo() -> u32 {
 ",
             "
 fn foo() -> u32 {
+    let <|>var_name = 2 + 2;
+    return var_name;
+}
+",
+        );
+    }
+
+    #[test]
+    fn test_introduce_var_does_not_add_extra_whitespace() {
+        check_assist(
+            introduce_variable,
+            "
+fn foo() -> u32 {
+
+
+    r<|>eturn 2 + 2;
+}
+",
+            "
+fn foo() -> u32 {
+
+
+    let <|>var_name = 2 + 2;
+    return var_name;
+}
+",
+        );
+
+        check_assist(
+            introduce_variable,
+            "
+fn foo() -> u32 {
+
+        r<|>eturn 2 + 2;
+}
+",
+            "
+fn foo() -> u32 {
+
+        let <|>var_name = 2 + 2;
+        return var_name;
+}
+",
+        );
+
+        check_assist(
+            introduce_variable,
+            "
+fn foo() -> u32 {
+    let foo = 1;
+
+    // bar
+
+
+    r<|>eturn 2 + 2;
+}
+",
+            "
+fn foo() -> u32 {
+    let foo = 1;
+
+    // bar
+
+
     let <|>var_name = 2 + 2;
     return var_name;
 }
@@ -429,4 +507,32 @@ fn main() {
 ",
         );
     }
+
+    // FIXME: This is not quite correct, but good enough(tm) for the sorting heuristic
+    #[test]
+    fn introduce_var_target() {
+        check_assist_target(
+            introduce_variable,
+            "
+fn foo() -> u32 {
+    r<|>eturn 2 + 2;
+}
+",
+            "2 + 2",
+        );
+
+        check_assist_range_target(
+            introduce_variable,
+            "
+fn main() {
+    let x = true;
+    let tuple = match x {
+        true => (<|>2 + 2<|>, true)
+        _ => (0, false)
+    };
+}
+",
+            "2 + 2",
+        );
+    }
 }