aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_assists/src/assists/introduce_variable.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_assists/src/assists/introduce_variable.rs')
-rw-r--r--crates/ra_assists/src/assists/introduce_variable.rs516
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 @@
1use format_buf::format;
2use hir::db::HirDatabase;
3use 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};
11use test_utils::tested_by;
12
13use crate::{Assist, AssistCtx, AssistId};
14
15pub(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.
82fn 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.
99fn 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)]
123mod 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 "
137fn foo() {
138 foo(<|>1 + 1<|>);
139}",
140 "
141fn 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 "
163fn foo() {
164 <|>1 + 1<|>;
165}",
166 "
167fn foo() {
168 let <|>var_name = 1 + 1;
169}",
170 );
171 check_assist_range(
172 introduce_variable,
173 "
174fn foo() {
175 <|>{ let x = 0; x }<|>
176 something_else();
177}",
178 "
179fn 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 "
191fn foo() {
192 <|>1<|> + 1;
193}",
194 "
195fn 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 "
208fn foo() {
209 bar(<|>1 + 1<|>)
210}",
211 "
212fn foo() {
213 let <|>var_name = 1 + 1;
214 bar(var_name)
215}",
216 );
217 check_assist_range(
218 introduce_variable,
219 "
220fn foo() {
221 <|>bar(1 + 1)<|>
222}",
223 "
224fn 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 "
236fn main() {
237 let x = true;
238 let tuple = match x {
239 true => (<|>2 + 2<|>, true)
240 _ => (0, false)
241 };
242}
243",
244 "
245fn 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 "
261fn 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 "
273fn 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 "
293fn main() {
294 let lambda = |x: u32| <|>x * 2<|>;
295}
296",
297 "
298fn 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 "
310fn main() {
311 let lambda = |x: u32| { <|>x * 2<|> };
312}
313",
314 "
315fn 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 "
327fn main() {
328 let o = <|>Some(true)<|>;
329}
330",
331 "
332fn 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 "
345fn main() {
346 let v = <|>bar.foo()<|>;
347}
348",
349 "
350fn 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 "
363fn foo() -> u32 {
364 <|>return 2 + 2<|>;
365}
366",
367 "
368fn 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 "
381fn foo() -> u32 {
382
383
384 <|>return 2 + 2<|>;
385}
386",
387 "
388fn 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 "
400fn foo() -> u32 {
401
402 <|>return 2 + 2<|>;
403}
404",
405 "
406fn 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 "
417fn foo() -> u32 {
418 let foo = 1;
419
420 // bar
421
422
423 <|>return 2 + 2<|>;
424}
425",
426 "
427fn 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 "
445fn main() {
446 let result = loop {
447 <|>break 2 + 2<|>;
448 };
449}
450",
451 "
452fn 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 "
467fn main() {
468 let v = <|>0f32 as u32<|>;
469}
470",
471 "
472fn 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 "
505fn 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}