aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src/handlers/extract_variable.rs
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2020-06-27 00:21:43 +0100
committerAleksey Kladov <[email protected]>2020-06-27 00:21:43 +0100
commitf5584668dba709a119f398de038b3f36085be5ff (patch)
treecadace8e7e0074454a7d9b428cf6af31443c8951 /crates/ra_assists/src/handlers/extract_variable.rs
parent7488cd6a1b3604d6b4f11a7f83b6006a34cda0c0 (diff)
introduce_variable -> extract_variable
Diffstat (limited to 'crates/ra_assists/src/handlers/extract_variable.rs')
-rw-r--r--crates/ra_assists/src/handlers/extract_variable.rs576
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 @@
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, 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(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.
124fn 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.
141fn 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)]
165mod 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#"
177fn foo() {
178 foo(<|>1 + 1<|>);
179}"#,
180 r#"
181fn 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#"
200fn foo() {
201 <|>1 + 1<|>;
202}"#,
203 r#"
204fn foo() {
205 let $0var_name = 1 + 1;
206}"#,
207 );
208 check_assist(
209 extract_variable,
210 "
211fn foo() {
212 <|>{ let x = 0; x }<|>
213 something_else();
214}",
215 "
216fn 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 "
228fn foo() {
229 <|>1<|> + 1;
230}",
231 "
232fn 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#"
245fn foo() {
246 bar(<|>1 + 1<|>)
247}
248"#,
249 r#"
250fn foo() {
251 let $0var_name = 1 + 1;
252 bar(var_name)
253}
254"#,
255 );
256 check_assist(
257 extract_variable,
258 r#"
259fn foo() {
260 <|>bar(1 + 1)<|>
261}
262"#,
263 r#"
264fn 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 "
277fn main() {
278 let x = true;
279 let tuple = match x {
280 true => (<|>2 + 2<|>, true)
281 _ => (0, false)
282 };
283}
284",
285 "
286fn 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 "
302fn 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 "
314fn 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 "
334fn main() {
335 let lambda = |x: u32| <|>x * 2<|>;
336}
337",
338 "
339fn 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 "
351fn main() {
352 let lambda = |x: u32| { <|>x * 2<|> };
353}
354",
355 "
356fn 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 "
368fn main() {
369 let o = <|>Some(true)<|>;
370}
371",
372 "
373fn 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 "
386fn main() {
387 let v = <|>bar.foo()<|>;
388}
389",
390 "
391fn 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 "
404fn foo() -> u32 {
405 <|>return 2 + 2<|>;
406}
407",
408 "
409fn 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 "
422fn foo() -> u32 {
423
424
425 <|>return 2 + 2<|>;
426}
427",
428 "
429fn 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 "
441fn foo() -> u32 {
442
443 <|>return 2 + 2<|>;
444}
445",
446 "
447fn foo() -> u32 {
448
449 let $0var_name = 2 + 2;
450 return var_name;
451}
452",
453 );
454
455 check_assist(
456 extract_variable,
457 "
458fn foo() -> u32 {
459 let foo = 1;
460
461 // bar
462
463
464 <|>return 2 + 2<|>;
465}
466",
467 "
468fn 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 "
486fn main() {
487 let result = loop {
488 <|>break 2 + 2<|>;
489 };
490}
491",
492 "
493fn 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 "
508fn main() {
509 let v = <|>0f32 as u32<|>;
510}
511",
512 "
513fn 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#"
526struct S {
527 foo: i32
528}
529
530fn main() {
531 S { foo: <|>1 + 1<|> }
532}
533"#,
534 r#"
535struct S {
536 foo: i32
537}
538
539fn 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 "
565fn 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}