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