]> git.lizzy.rs Git - rust.git/blob - crates/ra_assists/src/introduce_variable.rs
Merge #766
[rust.git] / crates / ra_assists / src / introduce_variable.rs
1 use hir::db::HirDatabase;
2 use ra_syntax::{
3     ast::{self, AstNode},
4     SyntaxKind::{
5         WHITESPACE, MATCH_ARM, LAMBDA_EXPR, PATH_EXPR, BREAK_EXPR, LOOP_EXPR, RETURN_EXPR, COMMENT
6     }, SyntaxNode, TextUnit,
7 };
8
9 use crate::{AssistCtx, Assist};
10
11 pub(crate) fn introduce_variable(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
12     let node = ctx.covering_node();
13     if !valid_covering_node(node) {
14         return None;
15     }
16     let expr = node.ancestors().filter_map(valid_target_expr).next()?;
17     let (anchor_stmt, wrap_in_block) = anchor_stmt(expr)?;
18     let indent = anchor_stmt.prev_sibling()?;
19     if indent.kind() != WHITESPACE {
20         return None;
21     }
22     ctx.build("introduce variable", move |edit| {
23         let mut buf = String::new();
24
25         let cursor_offset = if wrap_in_block {
26             buf.push_str("{ let var_name = ");
27             TextUnit::of_str("{ let ")
28         } else {
29             buf.push_str("let var_name = ");
30             TextUnit::of_str("let ")
31         };
32
33         expr.syntax().text().push_to(&mut buf);
34         let full_stmt = ast::ExprStmt::cast(anchor_stmt);
35         let is_full_stmt = if let Some(expr_stmt) = full_stmt {
36             Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax())
37         } else {
38             false
39         };
40         if is_full_stmt {
41             if !full_stmt.unwrap().has_semi() {
42                 buf.push_str(";");
43             }
44             edit.replace(expr.syntax().range(), buf);
45         } else {
46             buf.push_str(";");
47             indent.text().push_to(&mut buf);
48             edit.target(expr.syntax().range());
49             edit.replace(expr.syntax().range(), "var_name".to_string());
50             edit.insert(anchor_stmt.range().start(), buf);
51             if wrap_in_block {
52                 edit.insert(anchor_stmt.range().end(), " }");
53             }
54         }
55         edit.set_cursor(anchor_stmt.range().start() + cursor_offset);
56     })
57 }
58
59 fn valid_covering_node(node: &SyntaxNode) -> bool {
60     node.kind() != COMMENT
61 }
62 /// Check whether the node is a valid expression which can be extracted to a variable.
63 /// In general that's true for any expression, but in some cases that would produce invalid code.
64 fn valid_target_expr(node: &SyntaxNode) -> Option<&ast::Expr> {
65     match node.kind() {
66         PATH_EXPR => None,
67         BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()),
68         RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()),
69         LOOP_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()),
70         _ => ast::Expr::cast(node),
71     }
72 }
73
74 /// Returns the syntax node which will follow the freshly introduced var
75 /// and a boolean indicating whether we have to wrap it within a { } block
76 /// to produce correct code.
77 /// It can be a statement, the last in a block expression or a wanna be block
78 /// expression like a lambda or match arm.
79 fn anchor_stmt(expr: &ast::Expr) -> Option<(&SyntaxNode, bool)> {
80     expr.syntax().ancestors().find_map(|node| {
81         if ast::Stmt::cast(node).is_some() {
82             return Some((node, false));
83         }
84
85         if let Some(expr) = node.parent().and_then(ast::Block::cast).and_then(|it| it.expr()) {
86             if expr.syntax() == node {
87                 return Some((node, false));
88             }
89         }
90
91         if let Some(parent) = node.parent() {
92             if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR {
93                 return Some((node, true));
94             }
95         }
96
97         None
98     })
99 }
100
101 #[cfg(test)]
102 mod tests {
103     use super::*;
104     use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_range, check_assist_target, check_assist_range_target};
105
106     #[test]
107     fn test_introduce_var_simple() {
108         check_assist_range(
109             introduce_variable,
110             "
111 fn foo() {
112     foo(<|>1 + 1<|>);
113 }",
114             "
115 fn foo() {
116     let <|>var_name = 1 + 1;
117     foo(var_name);
118 }",
119         );
120     }
121
122     #[test]
123     fn test_introduce_var_expr_stmt() {
124         check_assist_range(
125             introduce_variable,
126             "
127 fn foo() {
128     <|>1 + 1<|>;
129 }",
130             "
131 fn foo() {
132     let <|>var_name = 1 + 1;
133 }",
134         );
135     }
136
137     #[test]
138     fn test_introduce_var_part_of_expr_stmt() {
139         check_assist_range(
140             introduce_variable,
141             "
142 fn foo() {
143     <|>1<|> + 1;
144 }",
145             "
146 fn foo() {
147     let <|>var_name = 1;
148     var_name + 1;
149 }",
150         );
151     }
152
153     #[test]
154     fn test_introduce_var_last_expr() {
155         check_assist_range(
156             introduce_variable,
157             "
158 fn foo() {
159     bar(<|>1 + 1<|>)
160 }",
161             "
162 fn foo() {
163     let <|>var_name = 1 + 1;
164     bar(var_name)
165 }",
166         );
167     }
168
169     #[test]
170     fn test_introduce_var_last_full_expr() {
171         check_assist_range(
172             introduce_variable,
173             "
174 fn foo() {
175     <|>bar(1 + 1)<|>
176 }",
177             "
178 fn foo() {
179     let <|>var_name = bar(1 + 1);
180     var_name
181 }",
182         );
183     }
184
185     #[test]
186     fn test_introduce_var_block_expr_second_to_last() {
187         check_assist_range(
188             introduce_variable,
189             "
190 fn foo() {
191     <|>{ let x = 0; x }<|>
192     something_else();
193 }",
194             "
195 fn foo() {
196     let <|>var_name = { let x = 0; x };
197     something_else();
198 }",
199         );
200     }
201
202     #[test]
203     fn test_introduce_var_in_match_arm_no_block() {
204         check_assist_range(
205             introduce_variable,
206             "
207 fn main() {
208     let x = true;
209     let tuple = match x {
210         true => (<|>2 + 2<|>, true)
211         _ => (0, false)
212     };
213 }
214 ",
215             "
216 fn main() {
217     let x = true;
218     let tuple = match x {
219         true => { let <|>var_name = 2 + 2; (var_name, true) }
220         _ => (0, false)
221     };
222 }
223 ",
224         );
225     }
226
227     #[test]
228     fn test_introduce_var_in_match_arm_with_block() {
229         check_assist_range(
230             introduce_variable,
231             "
232 fn main() {
233     let x = true;
234     let tuple = match x {
235         true => {
236             let y = 1;
237             (<|>2 + y<|>, true)
238         }
239         _ => (0, false)
240     };
241 }
242 ",
243             "
244 fn main() {
245     let x = true;
246     let tuple = match x {
247         true => {
248             let y = 1;
249             let <|>var_name = 2 + y;
250             (var_name, true)
251         }
252         _ => (0, false)
253     };
254 }
255 ",
256         );
257     }
258
259     #[test]
260     fn test_introduce_var_in_closure_no_block() {
261         check_assist_range(
262             introduce_variable,
263             "
264 fn main() {
265     let lambda = |x: u32| <|>x * 2<|>;
266 }
267 ",
268             "
269 fn main() {
270     let lambda = |x: u32| { let <|>var_name = x * 2; var_name };
271 }
272 ",
273         );
274     }
275
276     #[test]
277     fn test_introduce_var_in_closure_with_block() {
278         check_assist_range(
279             introduce_variable,
280             "
281 fn main() {
282     let lambda = |x: u32| { <|>x * 2<|> };
283 }
284 ",
285             "
286 fn main() {
287     let lambda = |x: u32| { let <|>var_name = x * 2; var_name };
288 }
289 ",
290         );
291     }
292
293     #[test]
294     fn test_introduce_var_path_simple() {
295         check_assist(
296             introduce_variable,
297             "
298 fn main() {
299     let o = S<|>ome(true);
300 }
301 ",
302             "
303 fn main() {
304     let <|>var_name = Some(true);
305     let o = var_name;
306 }
307 ",
308         );
309     }
310
311     #[test]
312     fn test_introduce_var_path_method() {
313         check_assist(
314             introduce_variable,
315             "
316 fn main() {
317     let v = b<|>ar.foo();
318 }
319 ",
320             "
321 fn main() {
322     let <|>var_name = bar.foo();
323     let v = var_name;
324 }
325 ",
326         );
327     }
328
329     #[test]
330     fn test_introduce_var_return() {
331         check_assist(
332             introduce_variable,
333             "
334 fn foo() -> u32 {
335     r<|>eturn 2 + 2;
336 }
337 ",
338             "
339 fn foo() -> u32 {
340     let <|>var_name = 2 + 2;
341     return var_name;
342 }
343 ",
344         );
345     }
346
347     #[test]
348     fn test_introduce_var_break() {
349         check_assist(
350             introduce_variable,
351             "
352 fn main() {
353     let result = loop {
354         b<|>reak 2 + 2;
355     };
356 }
357 ",
358             "
359 fn main() {
360     let result = loop {
361         let <|>var_name = 2 + 2;
362         break var_name;
363     };
364 }
365 ",
366         );
367     }
368
369     #[test]
370     fn test_introduce_var_for_cast() {
371         check_assist(
372             introduce_variable,
373             "
374 fn main() {
375     let v = 0f32 a<|>s u32;
376 }
377 ",
378             "
379 fn main() {
380     let <|>var_name = 0f32 as u32;
381     let v = var_name;
382 }
383 ",
384         );
385     }
386
387     #[test]
388     fn test_introduce_var_for_return_not_applicable() {
389         check_assist_not_applicable(
390             introduce_variable,
391             "
392 fn foo() {
393     r<|>eturn;
394 }
395 ",
396         );
397     }
398
399     #[test]
400     fn test_introduce_var_for_break_not_applicable() {
401         check_assist_not_applicable(
402             introduce_variable,
403             "
404 fn main() {
405     loop {
406         b<|>reak;
407     };
408 }
409 ",
410         );
411     }
412
413     #[test]
414     fn test_introduce_var_in_comment_not_applicable() {
415         check_assist_not_applicable(
416             introduce_variable,
417             "
418 fn main() {
419     let x = true;
420     let tuple = match x {
421         // c<|>omment
422         true => (2 + 2, true)
423         _ => (0, false)
424     };
425 }
426 ",
427         );
428     }
429
430     // FIXME: This is not quite correct, but good enough(tm) for the sorting heuristic
431     #[test]
432     fn introduce_var_target() {
433         check_assist_target(
434             introduce_variable,
435             "
436 fn foo() -> u32 {
437     r<|>eturn 2 + 2;
438 }
439 ",
440             "2 + 2",
441         );
442
443         check_assist_range_target(
444             introduce_variable,
445             "
446 fn main() {
447     let x = true;
448     let tuple = match x {
449         true => (<|>2 + 2<|>, true)
450         _ => (0, false)
451     };
452 }
453 ",
454             "2 + 2",
455         );
456     }
457 }