ide_assists/handlers/
unwrap_block.rs

1use syntax::{
2    AstNode, SyntaxKind, T, TextRange,
3    ast::{
4        self,
5        edit::{AstNodeEdit, IndentLevel},
6        make,
7    },
8};
9
10use crate::{AssistContext, AssistId, Assists};
11
12// Assist: unwrap_block
13//
14// This assist removes if...else, for, while and loop control statements to just keep the body.
15//
16// ```
17// fn foo() {
18//     if true {$0
19//         println!("foo");
20//     }
21// }
22// ```
23// ->
24// ```
25// fn foo() {
26//     println!("foo");
27// }
28// ```
29pub(crate) fn unwrap_block(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
30    let assist_id = AssistId::refactor_rewrite("unwrap_block");
31    let assist_label = "Unwrap block";
32    let l_curly_token = ctx.find_token_syntax_at_offset(T!['{'])?;
33    let mut block = ast::BlockExpr::cast(l_curly_token.parent_ancestors().nth(1)?)?;
34    let target = block.syntax().text_range();
35    let mut parent = block.syntax().parent()?;
36    if ast::MatchArm::can_cast(parent.kind()) {
37        parent = parent.ancestors().find(|it| ast::MatchExpr::can_cast(it.kind()))?
38    }
39
40    let kind = parent.kind();
41    if matches!(kind, SyntaxKind::STMT_LIST | SyntaxKind::EXPR_STMT) {
42        acc.add(assist_id, assist_label, target, |builder| {
43            builder.replace(block.syntax().text_range(), update_expr_string(block.to_string()));
44        })
45    } else if matches!(kind, SyntaxKind::LET_STMT) {
46        let parent = ast::LetStmt::cast(parent)?;
47        let pattern = ast::Pat::cast(parent.syntax().first_child()?)?;
48        let ty = parent.ty();
49        let list = block.stmt_list()?;
50        let replaced = match list.syntax().last_child() {
51            Some(last) => {
52                let stmts: Vec<ast::Stmt> = list.statements().collect();
53                let initializer = ast::Expr::cast(last)?;
54                let let_stmt = make::let_stmt(pattern, ty, Some(initializer));
55                if !stmts.is_empty() {
56                    let block = make::block_expr(stmts, None);
57                    format!("{}\n    {}", update_expr_string(block.to_string()), let_stmt)
58                } else {
59                    let_stmt.to_string()
60                }
61            }
62            None => {
63                let empty_tuple = make::ext::expr_unit();
64                make::let_stmt(pattern, ty, Some(empty_tuple)).to_string()
65            }
66        };
67        acc.add(assist_id, assist_label, target, |builder| {
68            builder.replace(parent.syntax().text_range(), replaced);
69        })
70    } else {
71        let parent = ast::Expr::cast(parent)?;
72        match parent.clone() {
73            ast::Expr::ForExpr(_) | ast::Expr::WhileExpr(_) | ast::Expr::LoopExpr(_) => (),
74            ast::Expr::MatchExpr(_) => block = block.dedent(IndentLevel(1)),
75            ast::Expr::IfExpr(if_expr) => {
76                let then_branch = if_expr.then_branch()?;
77                if then_branch == block {
78                    if let Some(ancestor) = if_expr.syntax().parent().and_then(ast::IfExpr::cast) {
79                        // For `else if` blocks
80                        let ancestor_then_branch = ancestor.then_branch()?;
81
82                        return acc.add(assist_id, assist_label, target, |edit| {
83                            let range_to_del_else_if = TextRange::new(
84                                ancestor_then_branch.syntax().text_range().end(),
85                                l_curly_token.text_range().start(),
86                            );
87                            let range_to_del_rest = TextRange::new(
88                                then_branch.syntax().text_range().end(),
89                                if_expr.syntax().text_range().end(),
90                            );
91
92                            edit.delete(range_to_del_rest);
93                            edit.delete(range_to_del_else_if);
94                            edit.replace(
95                                target,
96                                update_expr_string_without_newline(then_branch.to_string()),
97                            );
98                        });
99                    }
100                } else {
101                    return acc.add(assist_id, assist_label, target, |edit| {
102                        let range_to_del = TextRange::new(
103                            then_branch.syntax().text_range().end(),
104                            l_curly_token.text_range().start(),
105                        );
106
107                        edit.delete(range_to_del);
108                        edit.replace(target, update_expr_string_without_newline(block.to_string()));
109                    });
110                }
111            }
112            _ => return None,
113        };
114
115        acc.add(assist_id, assist_label, target, |builder| {
116            builder.replace(parent.syntax().text_range(), update_expr_string(block.to_string()));
117        })
118    }
119}
120
121fn update_expr_string(expr_string: String) -> String {
122    update_expr_string_with_pat(expr_string, &[' ', '\n'])
123}
124
125fn update_expr_string_without_newline(expr_string: String) -> String {
126    update_expr_string_with_pat(expr_string, &[' '])
127}
128
129fn update_expr_string_with_pat(expr_str: String, whitespace_pat: &[char]) -> String {
130    // Remove leading whitespace, index to remove the leading '{',
131    // then continue to remove leading whitespace.
132    // We cannot assume the `{` is the first character because there are block modifiers
133    // (`unsafe`, `async` etc.).
134    let after_open_brace_index = expr_str.find('{').map_or(0, |it| it + 1);
135    let expr_str = expr_str[after_open_brace_index..].trim_start_matches(whitespace_pat);
136
137    // Remove trailing whitespace, index [..expr_str.len() - 1] to remove the trailing '}',
138    // then continue to remove trailing whitespace.
139    let expr_str = expr_str.trim_end_matches(whitespace_pat);
140    let expr_str = expr_str[..expr_str.len() - 1].trim_end_matches(whitespace_pat);
141
142    expr_str
143        .lines()
144        .map(|line| line.replacen("    ", "", 1)) // Delete indentation
145        .collect::<Vec<String>>()
146        .join("\n")
147}
148
149#[cfg(test)]
150mod tests {
151    use crate::tests::{check_assist, check_assist_not_applicable};
152
153    use super::*;
154
155    #[test]
156    fn unwrap_tail_expr_block() {
157        check_assist(
158            unwrap_block,
159            r#"
160fn main() {
161    $0{
162        92
163    }
164}
165"#,
166            r#"
167fn main() {
168    92
169}
170"#,
171        )
172    }
173
174    #[test]
175    fn unwrap_stmt_expr_block() {
176        check_assist(
177            unwrap_block,
178            r#"
179fn main() {
180    $0{
181        92;
182    }
183    ()
184}
185"#,
186            r#"
187fn main() {
188    92;
189    ()
190}
191"#,
192        );
193        // Pedantically, we should add an `;` here...
194        check_assist(
195            unwrap_block,
196            r#"
197fn main() {
198    $0{
199        92
200    }
201    ()
202}
203"#,
204            r#"
205fn main() {
206    92
207    ()
208}
209"#,
210        );
211    }
212
213    #[test]
214    fn simple_if() {
215        check_assist(
216            unwrap_block,
217            r#"
218fn main() {
219    bar();
220    if true {$0
221        foo();
222
223        // comment
224        bar();
225    } else {
226        println!("bar");
227    }
228}
229"#,
230            r#"
231fn main() {
232    bar();
233    foo();
234
235    // comment
236    bar();
237}
238"#,
239        );
240    }
241
242    #[test]
243    fn simple_if_else() {
244        check_assist(
245            unwrap_block,
246            r#"
247fn main() {
248    bar();
249    if true {
250        foo();
251
252        // comment
253        bar();
254    } else {$0
255        println!("bar");
256    }
257}
258"#,
259            r#"
260fn main() {
261    bar();
262    if true {
263        foo();
264
265        // comment
266        bar();
267    }
268    println!("bar");
269}
270"#,
271        );
272    }
273
274    #[test]
275    fn simple_if_else_if() {
276        check_assist(
277            unwrap_block,
278            r#"
279fn main() {
280    // bar();
281    if true {
282        println!("true");
283
284        // comment
285        // bar();
286    } else if false {$0
287        println!("bar");
288    } else {
289        println!("foo");
290    }
291}
292"#,
293            r#"
294fn main() {
295    // bar();
296    if true {
297        println!("true");
298
299        // comment
300        // bar();
301    }
302    println!("bar");
303}
304"#,
305        );
306    }
307
308    #[test]
309    fn simple_if_else_if_nested() {
310        check_assist(
311            unwrap_block,
312            r#"
313fn main() {
314    // bar();
315    if true {
316        println!("true");
317
318        // comment
319        // bar();
320    } else if false {
321        println!("bar");
322    } else if true {$0
323        println!("foo");
324    }
325}
326"#,
327            r#"
328fn main() {
329    // bar();
330    if true {
331        println!("true");
332
333        // comment
334        // bar();
335    } else if false {
336        println!("bar");
337    }
338    println!("foo");
339}
340"#,
341        );
342    }
343
344    #[test]
345    fn simple_if_else_if_nested_else() {
346        check_assist(
347            unwrap_block,
348            r#"
349fn main() {
350    // bar();
351    if true {
352        println!("true");
353
354        // comment
355        // bar();
356    } else if false {
357        println!("bar");
358    } else if true {
359        println!("foo");
360    } else {$0
361        println!("else");
362    }
363}
364"#,
365            r#"
366fn main() {
367    // bar();
368    if true {
369        println!("true");
370
371        // comment
372        // bar();
373    } else if false {
374        println!("bar");
375    } else if true {
376        println!("foo");
377    }
378    println!("else");
379}
380"#,
381        );
382    }
383
384    #[test]
385    fn simple_if_else_if_nested_middle() {
386        check_assist(
387            unwrap_block,
388            r#"
389fn main() {
390    // bar();
391    if true {
392        println!("true");
393
394        // comment
395        // bar();
396    } else if false {
397        println!("bar");
398    } else if true {$0
399        println!("foo");
400    } else {
401        println!("else");
402    }
403}
404"#,
405            r#"
406fn main() {
407    // bar();
408    if true {
409        println!("true");
410
411        // comment
412        // bar();
413    } else if false {
414        println!("bar");
415    }
416    println!("foo");
417}
418"#,
419        );
420    }
421
422    #[test]
423    fn simple_if_bad_cursor_position() {
424        check_assist_not_applicable(
425            unwrap_block,
426            r#"
427fn main() {
428    bar();$0
429    if true {
430        foo();
431
432        // comment
433        bar();
434    } else {
435        println!("bar");
436    }
437}
438"#,
439        );
440    }
441
442    #[test]
443    fn simple_for() {
444        check_assist(
445            unwrap_block,
446            r#"
447fn main() {
448    for i in 0..5 {$0
449        if true {
450            foo();
451
452            // comment
453            bar();
454        } else {
455            println!("bar");
456        }
457    }
458}
459"#,
460            r#"
461fn main() {
462    if true {
463        foo();
464
465        // comment
466        bar();
467    } else {
468        println!("bar");
469    }
470}
471"#,
472        );
473    }
474
475    #[test]
476    fn simple_if_in_for() {
477        check_assist(
478            unwrap_block,
479            r#"
480fn main() {
481    for i in 0..5 {
482        if true {$0
483            foo();
484
485            // comment
486            bar();
487        } else {
488            println!("bar");
489        }
490    }
491}
492"#,
493            r#"
494fn main() {
495    for i in 0..5 {
496        foo();
497
498        // comment
499        bar();
500    }
501}
502"#,
503        );
504    }
505
506    #[test]
507    fn simple_loop() {
508        check_assist(
509            unwrap_block,
510            r#"
511fn main() {
512    loop {$0
513        if true {
514            foo();
515
516            // comment
517            bar();
518        } else {
519            println!("bar");
520        }
521    }
522}
523"#,
524            r#"
525fn main() {
526    if true {
527        foo();
528
529        // comment
530        bar();
531    } else {
532        println!("bar");
533    }
534}
535"#,
536        );
537    }
538
539    #[test]
540    fn simple_while() {
541        check_assist(
542            unwrap_block,
543            r#"
544fn main() {
545    while true {$0
546        if true {
547            foo();
548
549            // comment
550            bar();
551        } else {
552            println!("bar");
553        }
554    }
555}
556"#,
557            r#"
558fn main() {
559    if true {
560        foo();
561
562        // comment
563        bar();
564    } else {
565        println!("bar");
566    }
567}
568"#,
569        );
570    }
571
572    #[test]
573    fn unwrap_match_arm() {
574        check_assist(
575            unwrap_block,
576            r#"
577fn main() {
578    match rel_path {
579        Ok(rel_path) => {$0
580            let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
581            Some((*id, rel_path))
582        }
583        Err(_) => None,
584    }
585}
586"#,
587            r#"
588fn main() {
589    let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
590    Some((*id, rel_path))
591}
592"#,
593        );
594    }
595
596    #[test]
597    fn simple_if_in_while_bad_cursor_position() {
598        check_assist_not_applicable(
599            unwrap_block,
600            r#"
601fn main() {
602    while true {
603        if true {
604            foo();$0
605
606            // comment
607            bar();
608        } else {
609            println!("bar");
610        }
611    }
612}
613"#,
614        );
615    }
616
617    #[test]
618    fn simple_single_line() {
619        check_assist(
620            unwrap_block,
621            r#"
622fn main() {
623    {$0 0 }
624}
625"#,
626            r#"
627fn main() {
628    0
629}
630"#,
631        );
632    }
633
634    #[test]
635    fn simple_nested_block() {
636        check_assist(
637            unwrap_block,
638            r#"
639fn main() {
640    $0{
641        {
642            3
643        }
644    }
645}
646"#,
647            r#"
648fn main() {
649    {
650        3
651    }
652}
653"#,
654        );
655    }
656
657    #[test]
658    fn nested_single_line() {
659        check_assist(
660            unwrap_block,
661            r#"
662fn main() {
663    {$0 { println!("foo"); } }
664}
665"#,
666            r#"
667fn main() {
668    { println!("foo"); }
669}
670"#,
671        );
672
673        check_assist(
674            unwrap_block,
675            r#"
676fn main() {
677    {$0 { 0 } }
678}
679"#,
680            r#"
681fn main() {
682    { 0 }
683}
684"#,
685        );
686    }
687
688    #[test]
689    fn simple_if_single_line() {
690        check_assist(
691            unwrap_block,
692            r#"
693fn main() {
694    if true {$0 /* foo */ foo() } else { bar() /* bar */}
695}
696"#,
697            r#"
698fn main() {
699    /* foo */ foo()
700}
701"#,
702        );
703    }
704
705    #[test]
706    fn if_single_statement() {
707        check_assist(
708            unwrap_block,
709            r#"
710fn main() {
711    if true {$0
712        return 3;
713    }
714}
715"#,
716            r#"
717fn main() {
718    return 3;
719}
720"#,
721        );
722    }
723
724    #[test]
725    fn multiple_statements() {
726        check_assist(
727            unwrap_block,
728            r#"
729fn main() -> i32 {
730    if 2 > 1 {$0
731        let a = 5;
732        return 3;
733    }
734    5
735}
736"#,
737            r#"
738fn main() -> i32 {
739    let a = 5;
740    return 3;
741    5
742}
743"#,
744        );
745    }
746
747    #[test]
748    fn unwrap_block_in_let_initializers() {
749        // https://github.com/rust-lang/rust-analyzer/issues/13679
750        check_assist(
751            unwrap_block,
752            r#"
753fn main() {
754    let x = {$0};
755}
756"#,
757            r#"
758fn main() {
759    let x = ();
760}
761"#,
762        );
763        check_assist(
764            unwrap_block,
765            r#"
766fn main() {
767    let x = {$0
768        bar
769    };
770}
771"#,
772            r#"
773fn main() {
774    let x = bar;
775}
776"#,
777        );
778        check_assist(
779            unwrap_block,
780            r#"
781fn main() -> i32 {
782    let _ = {$01; 2};
783}
784"#,
785            r#"
786fn main() -> i32 {
787    1;
788    let _ = 2;
789}
790"#,
791        );
792        check_assist(
793            unwrap_block,
794            r#"
795fn main() -> i32 {
796    let mut a = {$01; 2};
797}
798"#,
799            r#"
800fn main() -> i32 {
801    1;
802    let mut a = 2;
803}
804"#,
805        );
806    }
807
808    #[test]
809    fn unwrap_if_in_let_initializers() {
810        // https://github.com/rust-lang/rust-analyzer/issues/13679
811        check_assist(
812            unwrap_block,
813            r#"
814fn main() {
815    let a = 1;
816    let x = if a - 1 == 0 {$0
817        foo
818    } else {
819        bar
820    };
821}
822"#,
823            r#"
824fn main() {
825    let a = 1;
826    let x = foo;
827}
828"#,
829        );
830    }
831
832    #[test]
833    fn unwrap_block_with_modifiers() {
834        // https://github.com/rust-lang/rust-analyzer/issues/17964
835        check_assist(
836            unwrap_block,
837            r#"
838fn main() {
839    unsafe $0{
840        bar;
841    }
842}
843"#,
844            r#"
845fn main() {
846    bar;
847}
848"#,
849        );
850        check_assist(
851            unwrap_block,
852            r#"
853fn main() {
854    async move $0{
855        bar;
856    }
857}
858"#,
859            r#"
860fn main() {
861    bar;
862}
863"#,
864        );
865        check_assist(
866            unwrap_block,
867            r#"
868fn main() {
869    try $0{
870        bar;
871    }
872}
873"#,
874            r#"
875fn main() {
876    bar;
877}
878"#,
879        );
880    }
881}