aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src/handlers/introduce_variable.rs
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2020-02-07 14:53:31 +0000
committerAleksey Kladov <[email protected]>2020-02-07 16:28:02 +0000
commit561b4b11ff1d87ea1ff2477dcba6ae1f396573a3 (patch)
tree0da58d08d5a2ff27f43c3eb6163ba9aced2f5782 /crates/ra_assists/src/handlers/introduce_variable.rs
parentaa64a84b493aa9c0b22f36b472a445d622cd2172 (diff)
Name assist handlers
Diffstat (limited to 'crates/ra_assists/src/handlers/introduce_variable.rs')
-rw-r--r--crates/ra_assists/src/handlers/introduce_variable.rs529
1 files changed, 529 insertions, 0 deletions
diff --git a/crates/ra_assists/src/handlers/introduce_variable.rs b/crates/ra_assists/src/handlers/introduce_variable.rs
new file mode 100644
index 000000000..7312ce687
--- /dev/null
+++ b/crates/ra_assists/src/handlers/introduce_variable.rs
@@ -0,0 +1,529 @@
1use format_buf::format;
2use ra_syntax::{
3 ast::{self, AstNode},
4 SyntaxKind::{
5 BLOCK_EXPR, BREAK_EXPR, COMMENT, LAMBDA_EXPR, LOOP_EXPR, MATCH_ARM, PATH_EXPR, RETURN_EXPR,
6 WHITESPACE,
7 },
8 SyntaxNode, TextUnit,
9};
10use test_utils::tested_by;
11
12use crate::{Assist, AssistCtx, AssistId};
13
14// Assist: introduce_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 var_name = (1 + 2);
27// var_name * 4;
28// }
29// ```
30pub(crate) fn introduce_variable(ctx: AssistCtx) -> Option<Assist> {
31 if ctx.frange.range.is_empty() {
32 return None;
33 }
34 let node = ctx.covering_element();
35 if node.kind() == COMMENT {
36 tested_by!(introduce_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 ctx.add_assist(AssistId("introduce_variable"), "Extract into variable", move |edit| {
46 let mut buf = String::new();
47
48 let cursor_offset = if wrap_in_block {
49 buf.push_str("{ let var_name = ");
50 TextUnit::of_str("{ let ")
51 } else {
52 buf.push_str("let var_name = ");
53 TextUnit::of_str("let ")
54 };
55 format!(buf, "{}", expr.syntax());
56 let full_stmt = ast::ExprStmt::cast(anchor_stmt.clone());
57 let is_full_stmt = if let Some(expr_stmt) = &full_stmt {
58 Some(expr.syntax().clone()) == expr_stmt.expr().map(|e| e.syntax().clone())
59 } else {
60 false
61 };
62 if is_full_stmt {
63 tested_by!(test_introduce_var_expr_stmt);
64 if !full_stmt.unwrap().has_semi() {
65 buf.push_str(";");
66 }
67 edit.replace(expr.syntax().text_range(), buf);
68 } else {
69 buf.push_str(";");
70
71 // We want to maintain the indent level,
72 // but we do not want to duplicate possible
73 // extra newlines in the indent block
74 let text = indent.text();
75 if text.starts_with('\n') {
76 buf.push_str("\n");
77 buf.push_str(text.trim_start_matches('\n'));
78 } else {
79 buf.push_str(text);
80 }
81
82 edit.target(expr.syntax().text_range());
83 edit.replace(expr.syntax().text_range(), "var_name".to_string());
84 edit.insert(anchor_stmt.text_range().start(), buf);
85 if wrap_in_block {
86 edit.insert(anchor_stmt.text_range().end(), " }");
87 }
88 }
89 edit.set_cursor(anchor_stmt.text_range().start() + cursor_offset);
90 })
91}
92
93/// Check whether the node is a valid expression which can be extracted to a variable.
94/// In general that's true for any expression, but in some cases that would produce invalid code.
95fn valid_target_expr(node: SyntaxNode) -> Option<ast::Expr> {
96 match node.kind() {
97 PATH_EXPR | LOOP_EXPR => None,
98 BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()),
99 RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()),
100 BLOCK_EXPR => {
101 ast::BlockExpr::cast(node).filter(|it| it.is_standalone()).map(ast::Expr::from)
102 }
103 _ => ast::Expr::cast(node),
104 }
105}
106
107/// Returns the syntax node which will follow the freshly introduced var
108/// and a boolean indicating whether we have to wrap it within a { } block
109/// to produce correct code.
110/// It can be a statement, the last in a block expression or a wanna be block
111/// expression like a lambda or match arm.
112fn anchor_stmt(expr: ast::Expr) -> Option<(SyntaxNode, bool)> {
113 expr.syntax().ancestors().find_map(|node| {
114 if let Some(expr) = node.parent().and_then(ast::Block::cast).and_then(|it| it.expr()) {
115 if expr.syntax() == &node {
116 tested_by!(test_introduce_var_last_expr);
117 return Some((node, false));
118 }
119 }
120
121 if let Some(parent) = node.parent() {
122 if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR {
123 return Some((node, true));
124 }
125 }
126
127 if ast::Stmt::cast(node.clone()).is_some() {
128 return Some((node, false));
129 }
130
131 None
132 })
133}
134
135#[cfg(test)]
136mod tests {
137 use test_utils::covers;
138
139 use crate::helpers::{
140 check_assist_range, check_assist_range_not_applicable, check_assist_range_target,
141 };
142
143 use super::*;
144
145 #[test]
146 fn test_introduce_var_simple() {
147 check_assist_range(
148 introduce_variable,
149 "
150fn foo() {
151 foo(<|>1 + 1<|>);
152}",
153 "
154fn foo() {
155 let <|>var_name = 1 + 1;
156 foo(var_name);
157}",
158 );
159 }
160
161 #[test]
162 fn introduce_var_in_comment_is_not_applicable() {
163 covers!(introduce_var_in_comment_is_not_applicable);
164 check_assist_range_not_applicable(
165 introduce_variable,
166 "fn main() { 1 + /* <|>comment<|> */ 1; }",
167 );
168 }
169
170 #[test]
171 fn test_introduce_var_expr_stmt() {
172 covers!(test_introduce_var_expr_stmt);
173 check_assist_range(
174 introduce_variable,
175 "
176fn foo() {
177 <|>1 + 1<|>;
178}",
179 "
180fn foo() {
181 let <|>var_name = 1 + 1;
182}",
183 );
184 check_assist_range(
185 introduce_variable,
186 "
187fn foo() {
188 <|>{ let x = 0; x }<|>
189 something_else();
190}",
191 "
192fn foo() {
193 let <|>var_name = { let x = 0; x };
194 something_else();
195}",
196 );
197 }
198
199 #[test]
200 fn test_introduce_var_part_of_expr_stmt() {
201 check_assist_range(
202 introduce_variable,
203 "
204fn foo() {
205 <|>1<|> + 1;
206}",
207 "
208fn foo() {
209 let <|>var_name = 1;
210 var_name + 1;
211}",
212 );
213 }
214
215 #[test]
216 fn test_introduce_var_last_expr() {
217 covers!(test_introduce_var_last_expr);
218 check_assist_range(
219 introduce_variable,
220 "
221fn foo() {
222 bar(<|>1 + 1<|>)
223}",
224 "
225fn foo() {
226 let <|>var_name = 1 + 1;
227 bar(var_name)
228}",
229 );
230 check_assist_range(
231 introduce_variable,
232 "
233fn foo() {
234 <|>bar(1 + 1)<|>
235}",
236 "
237fn foo() {
238 let <|>var_name = bar(1 + 1);
239 var_name
240}",
241 )
242 }
243
244 #[test]
245 fn test_introduce_var_in_match_arm_no_block() {
246 check_assist_range(
247 introduce_variable,
248 "
249fn main() {
250 let x = true;
251 let tuple = match x {
252 true => (<|>2 + 2<|>, true)
253 _ => (0, false)
254 };
255}
256",
257 "
258fn main() {
259 let x = true;
260 let tuple = match x {
261 true => { let <|>var_name = 2 + 2; (var_name, true) }
262 _ => (0, false)
263 };
264}
265",
266 );
267 }
268
269 #[test]
270 fn test_introduce_var_in_match_arm_with_block() {
271 check_assist_range(
272 introduce_variable,
273 "
274fn main() {
275 let x = true;
276 let tuple = match x {
277 true => {
278 let y = 1;
279 (<|>2 + y<|>, true)
280 }
281 _ => (0, false)
282 };
283}
284",
285 "
286fn main() {
287 let x = true;
288 let tuple = match x {
289 true => {
290 let y = 1;
291 let <|>var_name = 2 + y;
292 (var_name, true)
293 }
294 _ => (0, false)
295 };
296}
297",
298 );
299 }
300
301 #[test]
302 fn test_introduce_var_in_closure_no_block() {
303 check_assist_range(
304 introduce_variable,
305 "
306fn main() {
307 let lambda = |x: u32| <|>x * 2<|>;
308}
309",
310 "
311fn main() {
312 let lambda = |x: u32| { let <|>var_name = x * 2; var_name };
313}
314",
315 );
316 }
317
318 #[test]
319 fn test_introduce_var_in_closure_with_block() {
320 check_assist_range(
321 introduce_variable,
322 "
323fn main() {
324 let lambda = |x: u32| { <|>x * 2<|> };
325}
326",
327 "
328fn main() {
329 let lambda = |x: u32| { let <|>var_name = x * 2; var_name };
330}
331",
332 );
333 }
334
335 #[test]
336 fn test_introduce_var_path_simple() {
337 check_assist_range(
338 introduce_variable,
339 "
340fn main() {
341 let o = <|>Some(true)<|>;
342}
343",
344 "
345fn main() {
346 let <|>var_name = Some(true);
347 let o = var_name;
348}
349",
350 );
351 }
352
353 #[test]
354 fn test_introduce_var_path_method() {
355 check_assist_range(
356 introduce_variable,
357 "
358fn main() {
359 let v = <|>bar.foo()<|>;
360}
361",
362 "
363fn main() {
364 let <|>var_name = bar.foo();
365 let v = var_name;
366}
367",
368 );
369 }
370
371 #[test]
372 fn test_introduce_var_return() {
373 check_assist_range(
374 introduce_variable,
375 "
376fn foo() -> u32 {
377 <|>return 2 + 2<|>;
378}
379",
380 "
381fn foo() -> u32 {
382 let <|>var_name = 2 + 2;
383 return var_name;
384}
385",
386 );
387 }
388
389 #[test]
390 fn test_introduce_var_does_not_add_extra_whitespace() {
391 check_assist_range(
392 introduce_variable,
393 "
394fn foo() -> u32 {
395
396
397 <|>return 2 + 2<|>;
398}
399",
400 "
401fn foo() -> u32 {
402
403
404 let <|>var_name = 2 + 2;
405 return var_name;
406}
407",
408 );
409
410 check_assist_range(
411 introduce_variable,
412 "
413fn foo() -> u32 {
414
415 <|>return 2 + 2<|>;
416}
417",
418 "
419fn foo() -> u32 {
420
421 let <|>var_name = 2 + 2;
422 return var_name;
423}
424",
425 );
426
427 check_assist_range(
428 introduce_variable,
429 "
430fn foo() -> u32 {
431 let foo = 1;
432
433 // bar
434
435
436 <|>return 2 + 2<|>;
437}
438",
439 "
440fn foo() -> u32 {
441 let foo = 1;
442
443 // bar
444
445
446 let <|>var_name = 2 + 2;
447 return var_name;
448}
449",
450 );
451 }
452
453 #[test]
454 fn test_introduce_var_break() {
455 check_assist_range(
456 introduce_variable,
457 "
458fn main() {
459 let result = loop {
460 <|>break 2 + 2<|>;
461 };
462}
463",
464 "
465fn main() {
466 let result = loop {
467 let <|>var_name = 2 + 2;
468 break var_name;
469 };
470}
471",
472 );
473 }
474
475 #[test]
476 fn test_introduce_var_for_cast() {
477 check_assist_range(
478 introduce_variable,
479 "
480fn main() {
481 let v = <|>0f32 as u32<|>;
482}
483",
484 "
485fn main() {
486 let <|>var_name = 0f32 as u32;
487 let v = var_name;
488}
489",
490 );
491 }
492
493 #[test]
494 fn test_introduce_var_for_return_not_applicable() {
495 check_assist_range_not_applicable(introduce_variable, "fn foo() { <|>return<|>; } ");
496 }
497
498 #[test]
499 fn test_introduce_var_for_break_not_applicable() {
500 check_assist_range_not_applicable(
501 introduce_variable,
502 "fn main() { loop { <|>break<|>; }; }",
503 );
504 }
505
506 // FIXME: This is not quite correct, but good enough(tm) for the sorting heuristic
507 #[test]
508 fn introduce_var_target() {
509 check_assist_range_target(
510 introduce_variable,
511 "fn foo() -> u32 { <|>return 2 + 2<|>; }",
512 "2 + 2",
513 );
514
515 check_assist_range_target(
516 introduce_variable,
517 "
518fn main() {
519 let x = true;
520 let tuple = match x {
521 true => (<|>2 + 2<|>, true)
522 _ => (0, false)
523 };
524}
525",
526 "2 + 2",
527 );
528 }
529}