ide_assists/handlers/
convert_while_to_loop.rs

1use std::iter;
2
3use ide_db::syntax_helpers::node_ext::is_pattern_cond;
4use syntax::{
5    AstNode, T,
6    ast::{
7        self, HasLoopBody,
8        edit::{AstNodeEdit, IndentLevel},
9        make,
10    },
11    syntax_editor::{Element, Position},
12};
13
14use crate::{
15    AssistId,
16    assist_context::{AssistContext, Assists},
17    utils::invert_boolean_expression_legacy,
18};
19
20// Assist: convert_while_to_loop
21//
22// Replace a while with a loop.
23//
24// ```
25// fn main() {
26//     $0while cond {
27//         foo();
28//     }
29// }
30// ```
31// ->
32// ```
33// fn main() {
34//     loop {
35//         if !cond {
36//             break;
37//         }
38//         foo();
39//     }
40// }
41// ```
42pub(crate) fn convert_while_to_loop(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
43    let while_kw = ctx.find_token_syntax_at_offset(T![while])?;
44    let while_expr = while_kw.parent().and_then(ast::WhileExpr::cast)?;
45    let while_body = while_expr.loop_body()?;
46    let while_cond = while_expr.condition()?;
47    let l_curly = while_body.stmt_list()?.l_curly_token()?;
48
49    let target = while_expr.syntax().text_range();
50    acc.add(
51        AssistId::refactor_rewrite("convert_while_to_loop"),
52        "Convert while to loop",
53        target,
54        |builder| {
55            let mut edit = builder.make_editor(while_expr.syntax());
56            let while_indent_level = IndentLevel::from_node(while_expr.syntax());
57
58            let break_block = make::block_expr(
59                iter::once(make::expr_stmt(make::expr_break(None, None)).into()),
60                None,
61            )
62            .indent(IndentLevel(1));
63
64            edit.replace_all(
65                while_kw.syntax_element()..=while_cond.syntax().syntax_element(),
66                vec![make::token(T![loop]).syntax_element()],
67            );
68
69            if is_pattern_cond(while_cond.clone()) {
70                let then_branch = while_body.reset_indent().indent(IndentLevel(1));
71                let if_expr = make::expr_if(while_cond, then_branch, Some(break_block.into()));
72                let stmts = iter::once(make::expr_stmt(if_expr.into()).into());
73                let block_expr = make::block_expr(stmts, None);
74                edit.replace(while_body.syntax(), block_expr.indent(while_indent_level).syntax());
75            } else {
76                let if_cond = invert_boolean_expression_legacy(while_cond);
77                let if_expr = make::expr_if(if_cond, break_block, None).indent(while_indent_level);
78                if !while_body.syntax().text().contains_char('\n') {
79                    edit.insert(
80                        Position::after(&l_curly),
81                        make::tokens::whitespace(&format!("\n{while_indent_level}")),
82                    );
83                }
84                edit.insert_all(
85                    Position::after(&l_curly),
86                    vec![
87                        make::tokens::whitespace(&format!("\n{}", while_indent_level + 1)).into(),
88                        if_expr.syntax().syntax_element(),
89                    ],
90                );
91            };
92
93            builder.add_file_edits(ctx.vfs_file_id(), edit);
94        },
95    )
96}
97
98#[cfg(test)]
99mod tests {
100    use crate::tests::{check_assist, check_assist_not_applicable};
101
102    use super::*;
103
104    #[test]
105    fn convert_inside_fn() {
106        check_assist(
107            convert_while_to_loop,
108            r#"
109fn main() {
110    while$0 cond {
111        foo();
112    }
113}
114"#,
115            r#"
116fn main() {
117    loop {
118        if !cond {
119            break;
120        }
121        foo();
122    }
123}
124"#,
125        );
126    }
127
128    #[test]
129    fn convert_with_label() {
130        check_assist(
131            convert_while_to_loop,
132            r#"
133fn main() {
134    'x: while$0 cond {
135        foo();
136        break 'x
137    }
138}
139"#,
140            r#"
141fn main() {
142    'x: loop {
143        if !cond {
144            break;
145        }
146        foo();
147        break 'x
148    }
149}
150"#,
151        );
152
153        check_assist(
154            convert_while_to_loop,
155            r#"
156fn main() {
157    'x: while$0 let Some(x) = cond {
158        foo();
159        break 'x
160    }
161}
162"#,
163            r#"
164fn main() {
165    'x: loop {
166        if let Some(x) = cond {
167            foo();
168            break 'x
169        } else {
170            break;
171        }
172    }
173}
174"#,
175        );
176    }
177
178    #[test]
179    fn convert_with_attributes() {
180        check_assist(
181            convert_while_to_loop,
182            r#"
183fn main() {
184    #[allow(unused)]
185    while$0 cond {
186        foo();
187        break 'x
188    }
189}
190"#,
191            r#"
192fn main() {
193    #[allow(unused)]
194    loop {
195        if !cond {
196            break;
197        }
198        foo();
199        break 'x
200    }
201}
202"#,
203        );
204
205        check_assist(
206            convert_while_to_loop,
207            r#"
208fn main() {
209    #[allow(unused)]
210    #[deny(unsafe_code)]
211    while$0 let Some(x) = cond {
212        foo();
213    }
214}
215"#,
216            r#"
217fn main() {
218    #[allow(unused)]
219    #[deny(unsafe_code)]
220    loop {
221        if let Some(x) = cond {
222            foo();
223        } else {
224            break;
225        }
226    }
227}
228"#,
229        );
230    }
231
232    #[test]
233    fn convert_busy_wait() {
234        check_assist(
235            convert_while_to_loop,
236            r#"
237fn main() {
238    while$0 cond() {}
239}
240"#,
241            r#"
242fn main() {
243    loop {
244        if !cond() {
245            break;
246        }
247    }
248}
249"#,
250        );
251    }
252
253    #[test]
254    fn convert_trailing_expr() {
255        check_assist(
256            convert_while_to_loop,
257            r#"
258fn main() {
259    while$0 cond() {
260        bar()
261    }
262}
263"#,
264            r#"
265fn main() {
266    loop {
267        if !cond() {
268            break;
269        }
270        bar()
271    }
272}
273"#,
274        );
275    }
276
277    #[test]
278    fn convert_while_let() {
279        check_assist(
280            convert_while_to_loop,
281            r#"
282fn main() {
283    while$0 let Some(_) = foo() {
284        bar();
285    }
286}
287"#,
288            r#"
289fn main() {
290    loop {
291        if let Some(_) = foo() {
292            bar();
293        } else {
294            break;
295        }
296    }
297}
298"#,
299        );
300    }
301
302    #[test]
303    fn indentation() {
304        check_assist(
305            convert_while_to_loop,
306            r#"
307fn main() {
308    {
309        {
310            while$0 cond {
311                foo(
312                    "xxx",
313                );
314            }
315        }
316    }
317}
318"#,
319            r#"
320fn main() {
321    {
322        {
323            loop {
324                if !cond {
325                    break;
326                }
327                foo(
328                    "xxx",
329                );
330            }
331        }
332    }
333}
334"#,
335        );
336
337        check_assist(
338            convert_while_to_loop,
339            r#"
340fn main() {
341    {
342        {
343            while$0 let Some(_) = foo() {
344                bar(
345                    "xxx",
346                );
347            }
348        }
349    }
350}
351"#,
352            r#"
353fn main() {
354    {
355        {
356            loop {
357                if let Some(_) = foo() {
358                    bar(
359                        "xxx",
360                    );
361                } else {
362                    break;
363                }
364            }
365        }
366    }
367}
368"#,
369        );
370    }
371
372    #[test]
373    fn ignore_cursor_in_body() {
374        check_assist_not_applicable(
375            convert_while_to_loop,
376            r#"
377fn main() {
378    while cond {$0
379        bar();
380    }
381}
382"#,
383        );
384    }
385
386    #[test]
387    fn preserve_comments() {
388        check_assist(
389            convert_while_to_loop,
390            r#"
391fn main() {
392    let mut i = 0;
393
394    $0while i < 5 {
395        // comment 1
396        dbg!(i);
397        // comment 2
398        i += 1;
399        // comment 3
400    }
401}
402"#,
403            r#"
404fn main() {
405    let mut i = 0;
406
407    loop {
408        if i >= 5 {
409            break;
410        }
411        // comment 1
412        dbg!(i);
413        // comment 2
414        i += 1;
415        // comment 3
416    }
417}
418"#,
419        );
420
421        check_assist(
422            convert_while_to_loop,
423            r#"
424fn main() {
425    let v = vec![1, 2, 3];
426    let iter = v.iter();
427
428    $0while let Some(i) = iter.next() {
429        // comment 1
430        dbg!(i);
431        // comment 2
432    }
433}
434"#,
435            r#"
436fn main() {
437    let v = vec![1, 2, 3];
438    let iter = v.iter();
439
440    loop {
441        if let Some(i) = iter.next() {
442            // comment 1
443            dbg!(i);
444            // comment 2
445        } else {
446            break;
447        }
448    }
449}
450"#,
451        );
452    }
453}