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