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