aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src/handlers/extract_variable.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_assists/src/handlers/extract_variable.rs')
-rw-r--r--crates/ra_assists/src/handlers/extract_variable.rs202
1 files changed, 107 insertions, 95 deletions
diff --git a/crates/ra_assists/src/handlers/extract_variable.rs b/crates/ra_assists/src/handlers/extract_variable.rs
index c4150d2bb..b925a2884 100644
--- a/crates/ra_assists/src/handlers/extract_variable.rs
+++ b/crates/ra_assists/src/handlers/extract_variable.rs
@@ -2,14 +2,13 @@ use ra_syntax::{
2 ast::{self, AstNode}, 2 ast::{self, AstNode},
3 SyntaxKind::{ 3 SyntaxKind::{
4 BLOCK_EXPR, BREAK_EXPR, COMMENT, LAMBDA_EXPR, LOOP_EXPR, MATCH_ARM, PATH_EXPR, RETURN_EXPR, 4 BLOCK_EXPR, BREAK_EXPR, COMMENT, LAMBDA_EXPR, LOOP_EXPR, MATCH_ARM, PATH_EXPR, RETURN_EXPR,
5 WHITESPACE,
6 }, 5 },
7 SyntaxNode, 6 SyntaxNode,
8}; 7};
9use stdx::format_to; 8use stdx::format_to;
10use test_utils::mark; 9use test_utils::mark;
11 10
12use crate::{AssistContext, AssistId, Assists}; 11use crate::{AssistContext, AssistId, AssistKind, Assists};
13 12
14// Assist: extract_variable 13// Assist: extract_variable
15// 14//
@@ -36,87 +35,84 @@ pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext) -> Option
36 mark::hit!(extract_var_in_comment_is_not_applicable); 35 mark::hit!(extract_var_in_comment_is_not_applicable);
37 return None; 36 return None;
38 } 37 }
39 let expr = node.ancestors().find_map(valid_target_expr)?; 38 let to_extract = node.ancestors().find_map(valid_target_expr)?;
40 let (anchor_stmt, wrap_in_block) = anchor_stmt(expr.clone())?; 39 let anchor = Anchor::from(&to_extract)?;
41 let indent = anchor_stmt.prev_sibling_or_token()?.as_token()?.clone(); 40 let indent = anchor.syntax().prev_sibling_or_token()?.as_token()?.clone();
42 if indent.kind() != WHITESPACE { 41 let target = to_extract.syntax().text_range();
43 return None; 42 acc.add(
44 } 43 AssistId("extract_variable", AssistKind::RefactorExtract),
45 let target = expr.syntax().text_range(); 44 "Extract into variable",
46 acc.add(AssistId("extract_variable"), "Extract into variable", target, move |edit| { 45 target,
47 let field_shorthand = match expr.syntax().parent().and_then(ast::RecordField::cast) { 46 move |edit| {
48 Some(field) => field.name_ref(), 47 let field_shorthand =
49 None => None, 48 match to_extract.syntax().parent().and_then(ast::RecordExprField::cast) {
50 }; 49 Some(field) => field.name_ref(),
51 50 None => None,
52 let mut buf = String::new(); 51 };
53 52
54 let var_name = match &field_shorthand { 53 let mut buf = String::new();
55 Some(it) => it.to_string(), 54
56 None => "var_name".to_string(), 55 let var_name = match &field_shorthand {
57 }; 56 Some(it) => it.to_string(),
58 let expr_range = match &field_shorthand { 57 None => "var_name".to_string(),
59 Some(it) => it.syntax().text_range().cover(expr.syntax().text_range()), 58 };
60 None => expr.syntax().text_range(), 59 let expr_range = match &field_shorthand {
61 }; 60 Some(it) => it.syntax().text_range().cover(to_extract.syntax().text_range()),
62 61 None => to_extract.syntax().text_range(),
63 if wrap_in_block { 62 };
64 format_to!(buf, "{{ let {} = ", var_name); 63
65 } else { 64 if let Anchor::WrapInBlock(_) = anchor {
66 format_to!(buf, "let {} = ", var_name); 65 format_to!(buf, "{{ let {} = ", var_name);
67 }; 66 } else {
68 format_to!(buf, "{}", expr.syntax()); 67 format_to!(buf, "let {} = ", var_name);
69 68 };
70 let full_stmt = ast::ExprStmt::cast(anchor_stmt.clone()); 69 format_to!(buf, "{}", to_extract.syntax());
71 let is_full_stmt = if let Some(expr_stmt) = &full_stmt { 70
72 Some(expr.syntax().clone()) == expr_stmt.expr().map(|e| e.syntax().clone()) 71 if let Anchor::Replace(stmt) = anchor {
73 } else { 72 mark::hit!(test_extract_var_expr_stmt);
74 false 73 if stmt.semicolon_token().is_none() {
75 }; 74 buf.push_str(";");
76 if is_full_stmt { 75 }
77 mark::hit!(test_extract_var_expr_stmt); 76 match ctx.config.snippet_cap {
78 if full_stmt.unwrap().semicolon_token().is_none() { 77 Some(cap) => {
79 buf.push_str(";"); 78 let snip = buf
79 .replace(&format!("let {}", var_name), &format!("let $0{}", var_name));
80 edit.replace_snippet(cap, expr_range, snip)
81 }
82 None => edit.replace(expr_range, buf),
83 }
84 return;
80 } 85 }
86
87 buf.push_str(";");
88
89 // We want to maintain the indent level,
90 // but we do not want to duplicate possible
91 // extra newlines in the indent block
92 let text = indent.text();
93 if text.starts_with('\n') {
94 buf.push_str("\n");
95 buf.push_str(text.trim_start_matches('\n'));
96 } else {
97 buf.push_str(text);
98 }
99
100 edit.replace(expr_range, var_name.clone());
101 let offset = anchor.syntax().text_range().start();
81 match ctx.config.snippet_cap { 102 match ctx.config.snippet_cap {
82 Some(cap) => { 103 Some(cap) => {
83 let snip = 104 let snip =
84 buf.replace(&format!("let {}", var_name), &format!("let $0{}", var_name)); 105 buf.replace(&format!("let {}", var_name), &format!("let $0{}", var_name));
85 edit.replace_snippet(cap, expr_range, snip) 106 edit.insert_snippet(cap, offset, snip)
86 } 107 }
87 None => edit.replace(expr_range, buf), 108 None => edit.insert(offset, buf),
88 } 109 }
89 return;
90 }
91
92 buf.push_str(";");
93
94 // We want to maintain the indent level,
95 // but we do not want to duplicate possible
96 // extra newlines in the indent block
97 let text = indent.text();
98 if text.starts_with('\n') {
99 buf.push_str("\n");
100 buf.push_str(text.trim_start_matches('\n'));
101 } else {
102 buf.push_str(text);
103 }
104 110
105 edit.replace(expr_range, var_name.clone()); 111 if let Anchor::WrapInBlock(_) = anchor {
106 let offset = anchor_stmt.text_range().start(); 112 edit.insert(anchor.syntax().text_range().end(), " }");
107 match ctx.config.snippet_cap {
108 Some(cap) => {
109 let snip =
110 buf.replace(&format!("let {}", var_name), &format!("let $0{}", var_name));
111 edit.insert_snippet(cap, offset, snip)
112 } 113 }
113 None => edit.insert(offset, buf), 114 },
114 } 115 )
115
116 if wrap_in_block {
117 edit.insert(anchor_stmt.text_range().end(), " }");
118 }
119 })
120} 116}
121 117
122/// Check whether the node is a valid expression which can be extracted to a variable. 118/// Check whether the node is a valid expression which can be extracted to a variable.
@@ -133,32 +129,48 @@ fn valid_target_expr(node: SyntaxNode) -> Option<ast::Expr> {
133 } 129 }
134} 130}
135 131
136/// Returns the syntax node which will follow the freshly extractd var 132enum Anchor {
137/// and a boolean indicating whether we have to wrap it within a { } block 133 Before(SyntaxNode),
138/// to produce correct code. 134 Replace(ast::ExprStmt),
139/// It can be a statement, the last in a block expression or a wanna be block 135 WrapInBlock(SyntaxNode),
140/// expression like a lambda or match arm. 136}
141fn anchor_stmt(expr: ast::Expr) -> Option<(SyntaxNode, bool)> { 137
142 expr.syntax().ancestors().find_map(|node| { 138impl Anchor {
143 if let Some(expr) = node.parent().and_then(ast::BlockExpr::cast).and_then(|it| it.expr()) { 139 fn from(to_extract: &ast::Expr) -> Option<Anchor> {
144 if expr.syntax() == &node { 140 to_extract.syntax().ancestors().find_map(|node| {
145 mark::hit!(test_extract_var_last_expr); 141 if let Some(expr) =
146 return Some((node, false)); 142 node.parent().and_then(ast::BlockExpr::cast).and_then(|it| it.expr())
143 {
144 if expr.syntax() == &node {
145 mark::hit!(test_extract_var_last_expr);
146 return Some(Anchor::Before(node));
147 }
147 } 148 }
148 }
149 149
150 if let Some(parent) = node.parent() { 150 if let Some(parent) = node.parent() {
151 if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR { 151 if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR {
152 return Some((node, true)); 152 return Some(Anchor::WrapInBlock(node));
153 }
153 } 154 }
154 }
155 155
156 if ast::Stmt::cast(node.clone()).is_some() { 156 if let Some(stmt) = ast::Stmt::cast(node.clone()) {
157 return Some((node, false)); 157 if let ast::Stmt::ExprStmt(stmt) = stmt {
158 } 158 if stmt.expr().as_ref() == Some(to_extract) {
159 return Some(Anchor::Replace(stmt));
160 }
161 }
162 return Some(Anchor::Before(node));
163 }
164 None
165 })
166 }
159 167
160 None 168 fn syntax(&self) -> &SyntaxNode {
161 }) 169 match self {
170 Anchor::Before(it) | Anchor::WrapInBlock(it) => it,
171 Anchor::Replace(stmt) => stmt.syntax(),
172 }
173 }
162} 174}
163 175
164#[cfg(test)] 176#[cfg(test)]