aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src/handlers/extract_variable.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_assists/src/handlers/extract_variable.rs')
-rw-r--r--crates/ra_assists/src/handlers/extract_variable.rs581
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 @@
1use 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};
9use stdx::format_to;
10use test_utils::mark;
11
12use 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// ```
30pub(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.
129fn 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.
146fn 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)]
170mod 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#"
182fn foo() {
183 foo(<|>1 + 1<|>);
184}"#,
185 r#"
186fn 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#"
205fn foo() {
206 <|>1 + 1<|>;
207}"#,
208 r#"
209fn foo() {
210 let $0var_name = 1 + 1;
211}"#,
212 );
213 check_assist(
214 extract_variable,
215 "
216fn foo() {
217 <|>{ let x = 0; x }<|>
218 something_else();
219}",
220 "
221fn 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 "
233fn foo() {
234 <|>1<|> + 1;
235}",
236 "
237fn 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#"
250fn foo() {
251 bar(<|>1 + 1<|>)
252}
253"#,
254 r#"
255fn foo() {
256 let $0var_name = 1 + 1;
257 bar(var_name)
258}
259"#,
260 );
261 check_assist(
262 extract_variable,
263 r#"
264fn foo() {
265 <|>bar(1 + 1)<|>
266}
267"#,
268 r#"
269fn 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 "
282fn main() {
283 let x = true;
284 let tuple = match x {
285 true => (<|>2 + 2<|>, true)
286 _ => (0, false)
287 };
288}
289",
290 "
291fn 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 "
307fn 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 "
319fn 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 "
339fn main() {
340 let lambda = |x: u32| <|>x * 2<|>;
341}
342",
343 "
344fn 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 "
356fn main() {
357 let lambda = |x: u32| { <|>x * 2<|> };
358}
359",
360 "
361fn 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 "
373fn main() {
374 let o = <|>Some(true)<|>;
375}
376",
377 "
378fn 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 "
391fn main() {
392 let v = <|>bar.foo()<|>;
393}
394",
395 "
396fn 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 "
409fn foo() -> u32 {
410 <|>return 2 + 2<|>;
411}
412",
413 "
414fn 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 "
427fn foo() -> u32 {
428
429
430 <|>return 2 + 2<|>;
431}
432",
433 "
434fn 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 "
446fn foo() -> u32 {
447
448 <|>return 2 + 2<|>;
449}
450",
451 "
452fn foo() -> u32 {
453
454 let $0var_name = 2 + 2;
455 return var_name;
456}
457",
458 );
459
460 check_assist(
461 extract_variable,
462 "
463fn foo() -> u32 {
464 let foo = 1;
465
466 // bar
467
468
469 <|>return 2 + 2<|>;
470}
471",
472 "
473fn 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 "
491fn main() {
492 let result = loop {
493 <|>break 2 + 2<|>;
494 };
495}
496",
497 "
498fn 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 "
513fn main() {
514 let v = <|>0f32 as u32<|>;
515}
516",
517 "
518fn 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#"
531struct S {
532 foo: i32
533}
534
535fn main() {
536 S { foo: <|>1 + 1<|> }
537}
538"#,
539 r#"
540struct S {
541 foo: i32
542}
543
544fn 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 "
570fn 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}