diff options
Diffstat (limited to 'crates/ra_assists/src/handlers/extract_variable.rs')
-rw-r--r-- | crates/ra_assists/src/handlers/extract_variable.rs | 581 |
1 files changed, 581 insertions, 0 deletions
diff --git a/crates/ra_assists/src/handlers/extract_variable.rs b/crates/ra_assists/src/handlers/extract_variable.rs new file mode 100644 index 000000000..481baf1a4 --- /dev/null +++ b/crates/ra_assists/src/handlers/extract_variable.rs | |||
@@ -0,0 +1,581 @@ | |||
1 | use ra_syntax::{ | ||
2 | ast::{self, AstNode}, | ||
3 | SyntaxKind::{ | ||
4 | BLOCK_EXPR, BREAK_EXPR, COMMENT, LAMBDA_EXPR, LOOP_EXPR, MATCH_ARM, PATH_EXPR, RETURN_EXPR, | ||
5 | WHITESPACE, | ||
6 | }, | ||
7 | SyntaxNode, | ||
8 | }; | ||
9 | use stdx::format_to; | ||
10 | use test_utils::mark; | ||
11 | |||
12 | use crate::{AssistContext, AssistId, AssistKind, Assists}; | ||
13 | |||
14 | // Assist: extract_variable | ||
15 | // | ||
16 | // Extracts subexpression into a variable. | ||
17 | // | ||
18 | // ``` | ||
19 | // fn main() { | ||
20 | // <|>(1 + 2)<|> * 4; | ||
21 | // } | ||
22 | // ``` | ||
23 | // -> | ||
24 | // ``` | ||
25 | // fn main() { | ||
26 | // let $0var_name = (1 + 2); | ||
27 | // var_name * 4; | ||
28 | // } | ||
29 | // ``` | ||
30 | pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { | ||
31 | if ctx.frange.range.is_empty() { | ||
32 | return None; | ||
33 | } | ||
34 | let node = ctx.covering_element(); | ||
35 | if node.kind() == COMMENT { | ||
36 | mark::hit!(extract_var_in_comment_is_not_applicable); | ||
37 | return None; | ||
38 | } | ||
39 | let expr = node.ancestors().find_map(valid_target_expr)?; | ||
40 | let (anchor_stmt, wrap_in_block) = anchor_stmt(expr.clone())?; | ||
41 | let indent = anchor_stmt.prev_sibling_or_token()?.as_token()?.clone(); | ||
42 | if indent.kind() != WHITESPACE { | ||
43 | return None; | ||
44 | } | ||
45 | let target = expr.syntax().text_range(); | ||
46 | acc.add( | ||
47 | AssistId("extract_variable", AssistKind::RefactorExtract), | ||
48 | "Extract into variable", | ||
49 | target, | ||
50 | move |edit| { | ||
51 | let field_shorthand = match expr.syntax().parent().and_then(ast::RecordField::cast) { | ||
52 | Some(field) => field.name_ref(), | ||
53 | None => None, | ||
54 | }; | ||
55 | |||
56 | let mut buf = String::new(); | ||
57 | |||
58 | let var_name = match &field_shorthand { | ||
59 | Some(it) => it.to_string(), | ||
60 | None => "var_name".to_string(), | ||
61 | }; | ||
62 | let expr_range = match &field_shorthand { | ||
63 | Some(it) => it.syntax().text_range().cover(expr.syntax().text_range()), | ||
64 | None => expr.syntax().text_range(), | ||
65 | }; | ||
66 | |||
67 | if wrap_in_block { | ||
68 | format_to!(buf, "{{ let {} = ", var_name); | ||
69 | } else { | ||
70 | format_to!(buf, "let {} = ", var_name); | ||
71 | }; | ||
72 | format_to!(buf, "{}", expr.syntax()); | ||
73 | |||
74 | let full_stmt = ast::ExprStmt::cast(anchor_stmt.clone()); | ||
75 | let is_full_stmt = if let Some(expr_stmt) = &full_stmt { | ||
76 | Some(expr.syntax().clone()) == expr_stmt.expr().map(|e| e.syntax().clone()) | ||
77 | } else { | ||
78 | false | ||
79 | }; | ||
80 | if is_full_stmt { | ||
81 | mark::hit!(test_extract_var_expr_stmt); | ||
82 | if full_stmt.unwrap().semicolon_token().is_none() { | ||
83 | buf.push_str(";"); | ||
84 | } | ||
85 | match ctx.config.snippet_cap { | ||
86 | Some(cap) => { | ||
87 | let snip = buf | ||
88 | .replace(&format!("let {}", var_name), &format!("let $0{}", var_name)); | ||
89 | edit.replace_snippet(cap, expr_range, snip) | ||
90 | } | ||
91 | None => edit.replace(expr_range, buf), | ||
92 | } | ||
93 | return; | ||
94 | } | ||
95 | |||
96 | buf.push_str(";"); | ||
97 | |||
98 | // We want to maintain the indent level, | ||
99 | // but we do not want to duplicate possible | ||
100 | // extra newlines in the indent block | ||
101 | let text = indent.text(); | ||
102 | if text.starts_with('\n') { | ||
103 | buf.push_str("\n"); | ||
104 | buf.push_str(text.trim_start_matches('\n')); | ||
105 | } else { | ||
106 | buf.push_str(text); | ||
107 | } | ||
108 | |||
109 | edit.replace(expr_range, var_name.clone()); | ||
110 | let offset = anchor_stmt.text_range().start(); | ||
111 | match ctx.config.snippet_cap { | ||
112 | Some(cap) => { | ||
113 | let snip = | ||
114 | buf.replace(&format!("let {}", var_name), &format!("let $0{}", var_name)); | ||
115 | edit.insert_snippet(cap, offset, snip) | ||
116 | } | ||
117 | None => edit.insert(offset, buf), | ||
118 | } | ||
119 | |||
120 | if wrap_in_block { | ||
121 | edit.insert(anchor_stmt.text_range().end(), " }"); | ||
122 | } | ||
123 | }, | ||
124 | ) | ||
125 | } | ||
126 | |||
127 | /// Check whether the node is a valid expression which can be extracted to a variable. | ||
128 | /// In general that's true for any expression, but in some cases that would produce invalid code. | ||
129 | fn valid_target_expr(node: SyntaxNode) -> Option<ast::Expr> { | ||
130 | match node.kind() { | ||
131 | PATH_EXPR | LOOP_EXPR => None, | ||
132 | BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()), | ||
133 | RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()), | ||
134 | BLOCK_EXPR => { | ||
135 | ast::BlockExpr::cast(node).filter(|it| it.is_standalone()).map(ast::Expr::from) | ||
136 | } | ||
137 | _ => ast::Expr::cast(node), | ||
138 | } | ||
139 | } | ||
140 | |||
141 | /// Returns the syntax node which will follow the freshly extractd var | ||
142 | /// and a boolean indicating whether we have to wrap it within a { } block | ||
143 | /// to produce correct code. | ||
144 | /// It can be a statement, the last in a block expression or a wanna be block | ||
145 | /// expression like a lambda or match arm. | ||
146 | fn anchor_stmt(expr: ast::Expr) -> Option<(SyntaxNode, bool)> { | ||
147 | expr.syntax().ancestors().find_map(|node| { | ||
148 | if let Some(expr) = node.parent().and_then(ast::BlockExpr::cast).and_then(|it| it.expr()) { | ||
149 | if expr.syntax() == &node { | ||
150 | mark::hit!(test_extract_var_last_expr); | ||
151 | return Some((node, false)); | ||
152 | } | ||
153 | } | ||
154 | |||
155 | if let Some(parent) = node.parent() { | ||
156 | if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR { | ||
157 | return Some((node, true)); | ||
158 | } | ||
159 | } | ||
160 | |||
161 | if ast::Stmt::cast(node.clone()).is_some() { | ||
162 | return Some((node, false)); | ||
163 | } | ||
164 | |||
165 | None | ||
166 | }) | ||
167 | } | ||
168 | |||
169 | #[cfg(test)] | ||
170 | mod tests { | ||
171 | use test_utils::mark; | ||
172 | |||
173 | use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target}; | ||
174 | |||
175 | use super::*; | ||
176 | |||
177 | #[test] | ||
178 | fn test_extract_var_simple() { | ||
179 | check_assist( | ||
180 | extract_variable, | ||
181 | r#" | ||
182 | fn foo() { | ||
183 | foo(<|>1 + 1<|>); | ||
184 | }"#, | ||
185 | r#" | ||
186 | fn foo() { | ||
187 | let $0var_name = 1 + 1; | ||
188 | foo(var_name); | ||
189 | }"#, | ||
190 | ); | ||
191 | } | ||
192 | |||
193 | #[test] | ||
194 | fn extract_var_in_comment_is_not_applicable() { | ||
195 | mark::check!(extract_var_in_comment_is_not_applicable); | ||
196 | check_assist_not_applicable(extract_variable, "fn main() { 1 + /* <|>comment<|> */ 1; }"); | ||
197 | } | ||
198 | |||
199 | #[test] | ||
200 | fn test_extract_var_expr_stmt() { | ||
201 | mark::check!(test_extract_var_expr_stmt); | ||
202 | check_assist( | ||
203 | extract_variable, | ||
204 | r#" | ||
205 | fn foo() { | ||
206 | <|>1 + 1<|>; | ||
207 | }"#, | ||
208 | r#" | ||
209 | fn foo() { | ||
210 | let $0var_name = 1 + 1; | ||
211 | }"#, | ||
212 | ); | ||
213 | check_assist( | ||
214 | extract_variable, | ||
215 | " | ||
216 | fn foo() { | ||
217 | <|>{ let x = 0; x }<|> | ||
218 | something_else(); | ||
219 | }", | ||
220 | " | ||
221 | fn foo() { | ||
222 | let $0var_name = { let x = 0; x }; | ||
223 | something_else(); | ||
224 | }", | ||
225 | ); | ||
226 | } | ||
227 | |||
228 | #[test] | ||
229 | fn test_extract_var_part_of_expr_stmt() { | ||
230 | check_assist( | ||
231 | extract_variable, | ||
232 | " | ||
233 | fn foo() { | ||
234 | <|>1<|> + 1; | ||
235 | }", | ||
236 | " | ||
237 | fn foo() { | ||
238 | let $0var_name = 1; | ||
239 | var_name + 1; | ||
240 | }", | ||
241 | ); | ||
242 | } | ||
243 | |||
244 | #[test] | ||
245 | fn test_extract_var_last_expr() { | ||
246 | mark::check!(test_extract_var_last_expr); | ||
247 | check_assist( | ||
248 | extract_variable, | ||
249 | r#" | ||
250 | fn foo() { | ||
251 | bar(<|>1 + 1<|>) | ||
252 | } | ||
253 | "#, | ||
254 | r#" | ||
255 | fn foo() { | ||
256 | let $0var_name = 1 + 1; | ||
257 | bar(var_name) | ||
258 | } | ||
259 | "#, | ||
260 | ); | ||
261 | check_assist( | ||
262 | extract_variable, | ||
263 | r#" | ||
264 | fn foo() { | ||
265 | <|>bar(1 + 1)<|> | ||
266 | } | ||
267 | "#, | ||
268 | r#" | ||
269 | fn foo() { | ||
270 | let $0var_name = bar(1 + 1); | ||
271 | var_name | ||
272 | } | ||
273 | "#, | ||
274 | ) | ||
275 | } | ||
276 | |||
277 | #[test] | ||
278 | fn test_extract_var_in_match_arm_no_block() { | ||
279 | check_assist( | ||
280 | extract_variable, | ||
281 | " | ||
282 | fn main() { | ||
283 | let x = true; | ||
284 | let tuple = match x { | ||
285 | true => (<|>2 + 2<|>, true) | ||
286 | _ => (0, false) | ||
287 | }; | ||
288 | } | ||
289 | ", | ||
290 | " | ||
291 | fn main() { | ||
292 | let x = true; | ||
293 | let tuple = match x { | ||
294 | true => { let $0var_name = 2 + 2; (var_name, true) } | ||
295 | _ => (0, false) | ||
296 | }; | ||
297 | } | ||
298 | ", | ||
299 | ); | ||
300 | } | ||
301 | |||
302 | #[test] | ||
303 | fn test_extract_var_in_match_arm_with_block() { | ||
304 | check_assist( | ||
305 | extract_variable, | ||
306 | " | ||
307 | fn main() { | ||
308 | let x = true; | ||
309 | let tuple = match x { | ||
310 | true => { | ||
311 | let y = 1; | ||
312 | (<|>2 + y<|>, true) | ||
313 | } | ||
314 | _ => (0, false) | ||
315 | }; | ||
316 | } | ||
317 | ", | ||
318 | " | ||
319 | fn main() { | ||
320 | let x = true; | ||
321 | let tuple = match x { | ||
322 | true => { | ||
323 | let y = 1; | ||
324 | let $0var_name = 2 + y; | ||
325 | (var_name, true) | ||
326 | } | ||
327 | _ => (0, false) | ||
328 | }; | ||
329 | } | ||
330 | ", | ||
331 | ); | ||
332 | } | ||
333 | |||
334 | #[test] | ||
335 | fn test_extract_var_in_closure_no_block() { | ||
336 | check_assist( | ||
337 | extract_variable, | ||
338 | " | ||
339 | fn main() { | ||
340 | let lambda = |x: u32| <|>x * 2<|>; | ||
341 | } | ||
342 | ", | ||
343 | " | ||
344 | fn main() { | ||
345 | let lambda = |x: u32| { let $0var_name = x * 2; var_name }; | ||
346 | } | ||
347 | ", | ||
348 | ); | ||
349 | } | ||
350 | |||
351 | #[test] | ||
352 | fn test_extract_var_in_closure_with_block() { | ||
353 | check_assist( | ||
354 | extract_variable, | ||
355 | " | ||
356 | fn main() { | ||
357 | let lambda = |x: u32| { <|>x * 2<|> }; | ||
358 | } | ||
359 | ", | ||
360 | " | ||
361 | fn main() { | ||
362 | let lambda = |x: u32| { let $0var_name = x * 2; var_name }; | ||
363 | } | ||
364 | ", | ||
365 | ); | ||
366 | } | ||
367 | |||
368 | #[test] | ||
369 | fn test_extract_var_path_simple() { | ||
370 | check_assist( | ||
371 | extract_variable, | ||
372 | " | ||
373 | fn main() { | ||
374 | let o = <|>Some(true)<|>; | ||
375 | } | ||
376 | ", | ||
377 | " | ||
378 | fn main() { | ||
379 | let $0var_name = Some(true); | ||
380 | let o = var_name; | ||
381 | } | ||
382 | ", | ||
383 | ); | ||
384 | } | ||
385 | |||
386 | #[test] | ||
387 | fn test_extract_var_path_method() { | ||
388 | check_assist( | ||
389 | extract_variable, | ||
390 | " | ||
391 | fn main() { | ||
392 | let v = <|>bar.foo()<|>; | ||
393 | } | ||
394 | ", | ||
395 | " | ||
396 | fn main() { | ||
397 | let $0var_name = bar.foo(); | ||
398 | let v = var_name; | ||
399 | } | ||
400 | ", | ||
401 | ); | ||
402 | } | ||
403 | |||
404 | #[test] | ||
405 | fn test_extract_var_return() { | ||
406 | check_assist( | ||
407 | extract_variable, | ||
408 | " | ||
409 | fn foo() -> u32 { | ||
410 | <|>return 2 + 2<|>; | ||
411 | } | ||
412 | ", | ||
413 | " | ||
414 | fn foo() -> u32 { | ||
415 | let $0var_name = 2 + 2; | ||
416 | return var_name; | ||
417 | } | ||
418 | ", | ||
419 | ); | ||
420 | } | ||
421 | |||
422 | #[test] | ||
423 | fn test_extract_var_does_not_add_extra_whitespace() { | ||
424 | check_assist( | ||
425 | extract_variable, | ||
426 | " | ||
427 | fn foo() -> u32 { | ||
428 | |||
429 | |||
430 | <|>return 2 + 2<|>; | ||
431 | } | ||
432 | ", | ||
433 | " | ||
434 | fn foo() -> u32 { | ||
435 | |||
436 | |||
437 | let $0var_name = 2 + 2; | ||
438 | return var_name; | ||
439 | } | ||
440 | ", | ||
441 | ); | ||
442 | |||
443 | check_assist( | ||
444 | extract_variable, | ||
445 | " | ||
446 | fn foo() -> u32 { | ||
447 | |||
448 | <|>return 2 + 2<|>; | ||
449 | } | ||
450 | ", | ||
451 | " | ||
452 | fn foo() -> u32 { | ||
453 | |||
454 | let $0var_name = 2 + 2; | ||
455 | return var_name; | ||
456 | } | ||
457 | ", | ||
458 | ); | ||
459 | |||
460 | check_assist( | ||
461 | extract_variable, | ||
462 | " | ||
463 | fn foo() -> u32 { | ||
464 | let foo = 1; | ||
465 | |||
466 | // bar | ||
467 | |||
468 | |||
469 | <|>return 2 + 2<|>; | ||
470 | } | ||
471 | ", | ||
472 | " | ||
473 | fn foo() -> u32 { | ||
474 | let foo = 1; | ||
475 | |||
476 | // bar | ||
477 | |||
478 | |||
479 | let $0var_name = 2 + 2; | ||
480 | return var_name; | ||
481 | } | ||
482 | ", | ||
483 | ); | ||
484 | } | ||
485 | |||
486 | #[test] | ||
487 | fn test_extract_var_break() { | ||
488 | check_assist( | ||
489 | extract_variable, | ||
490 | " | ||
491 | fn main() { | ||
492 | let result = loop { | ||
493 | <|>break 2 + 2<|>; | ||
494 | }; | ||
495 | } | ||
496 | ", | ||
497 | " | ||
498 | fn main() { | ||
499 | let result = loop { | ||
500 | let $0var_name = 2 + 2; | ||
501 | break var_name; | ||
502 | }; | ||
503 | } | ||
504 | ", | ||
505 | ); | ||
506 | } | ||
507 | |||
508 | #[test] | ||
509 | fn test_extract_var_for_cast() { | ||
510 | check_assist( | ||
511 | extract_variable, | ||
512 | " | ||
513 | fn main() { | ||
514 | let v = <|>0f32 as u32<|>; | ||
515 | } | ||
516 | ", | ||
517 | " | ||
518 | fn main() { | ||
519 | let $0var_name = 0f32 as u32; | ||
520 | let v = var_name; | ||
521 | } | ||
522 | ", | ||
523 | ); | ||
524 | } | ||
525 | |||
526 | #[test] | ||
527 | fn extract_var_field_shorthand() { | ||
528 | check_assist( | ||
529 | extract_variable, | ||
530 | r#" | ||
531 | struct S { | ||
532 | foo: i32 | ||
533 | } | ||
534 | |||
535 | fn main() { | ||
536 | S { foo: <|>1 + 1<|> } | ||
537 | } | ||
538 | "#, | ||
539 | r#" | ||
540 | struct S { | ||
541 | foo: i32 | ||
542 | } | ||
543 | |||
544 | fn main() { | ||
545 | let $0foo = 1 + 1; | ||
546 | S { foo } | ||
547 | } | ||
548 | "#, | ||
549 | ) | ||
550 | } | ||
551 | |||
552 | #[test] | ||
553 | fn test_extract_var_for_return_not_applicable() { | ||
554 | check_assist_not_applicable(extract_variable, "fn foo() { <|>return<|>; } "); | ||
555 | } | ||
556 | |||
557 | #[test] | ||
558 | fn test_extract_var_for_break_not_applicable() { | ||
559 | check_assist_not_applicable(extract_variable, "fn main() { loop { <|>break<|>; }; }"); | ||
560 | } | ||
561 | |||
562 | // FIXME: This is not quite correct, but good enough(tm) for the sorting heuristic | ||
563 | #[test] | ||
564 | fn extract_var_target() { | ||
565 | check_assist_target(extract_variable, "fn foo() -> u32 { <|>return 2 + 2<|>; }", "2 + 2"); | ||
566 | |||
567 | check_assist_target( | ||
568 | extract_variable, | ||
569 | " | ||
570 | fn main() { | ||
571 | let x = true; | ||
572 | let tuple = match x { | ||
573 | true => (<|>2 + 2<|>, true) | ||
574 | _ => (0, false) | ||
575 | }; | ||
576 | } | ||
577 | ", | ||
578 | "2 + 2", | ||
579 | ); | ||
580 | } | ||
581 | } | ||