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