Skip to main content

ide_assists/handlers/
add_label_to_loop.rs

1use ide_db::{
2    source_change::SourceChangeBuilder, syntax_helpers::node_ext::for_each_break_and_continue_expr,
3};
4use syntax::{
5    SyntaxToken, T,
6    ast::{self, AstNode, HasLoopBody},
7    syntax_editor::{Position, SyntaxEditor},
8};
9
10use crate::{AssistContext, AssistId, Assists};
11
12// Assist: add_label_to_loop
13//
14// Adds a label to a loop.
15//
16// ```
17// fn main() {
18//     loop$0 {
19//         break;
20//         continue;
21//     }
22// }
23// ```
24// ->
25// ```
26// fn main() {
27//     ${1:'l}: loop {
28//         break ${2:'l};
29//         continue ${0:'l};
30//     }
31// }
32// ```
33pub(crate) fn add_label_to_loop(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
34    let loop_expr = ctx.find_node_at_offset::<ast::AnyHasLoopBody>()?;
35    let loop_kw = loop_token(&loop_expr)?;
36    if loop_expr.label().is_some() || !loop_kw.text_range().contains_inclusive(ctx.offset()) {
37        return None;
38    }
39
40    acc.add(
41        AssistId::generate("add_label_to_loop"),
42        "Add Label",
43        loop_expr.syntax().text_range(),
44        |builder| {
45            let editor = builder.make_editor(loop_expr.syntax());
46            let make = editor.make();
47
48            let label = make.lifetime("'l");
49            let elements = vec![
50                label.syntax().clone().into(),
51                make.token(T![:]).into(),
52                make.whitespace(" ").into(),
53            ];
54            editor.insert_all(Position::before(&loop_kw), elements);
55
56            if let Some(cap) = ctx.config.snippet_cap {
57                editor.add_annotation(label.syntax(), builder.make_placeholder_snippet(cap));
58            }
59
60            let loop_body = loop_expr.loop_body().and_then(|it| it.stmt_list());
61            for_each_break_and_continue_expr(loop_expr.label(), loop_body, &mut |expr| {
62                let token = match expr {
63                    ast::Expr::BreakExpr(break_expr) => break_expr.break_token(),
64                    ast::Expr::ContinueExpr(continue_expr) => continue_expr.continue_token(),
65                    _ => return,
66                };
67                if let Some(token) = token {
68                    insert_label_after_token(&editor, &token, ctx, builder);
69                }
70            });
71
72            builder.add_file_edits(ctx.vfs_file_id(), editor);
73            builder.rename();
74        },
75    )
76}
77
78fn loop_token(loop_expr: &ast::AnyHasLoopBody) -> Option<syntax::SyntaxToken> {
79    loop_expr
80        .syntax()
81        .children_with_tokens()
82        .filter_map(|it| it.into_token())
83        .find(|it| matches!(it.kind(), T![for] | T![loop] | T![while]))
84}
85
86fn insert_label_after_token(
87    editor: &SyntaxEditor,
88    token: &SyntaxToken,
89    ctx: &AssistContext<'_, '_>,
90    builder: &mut SourceChangeBuilder,
91) {
92    let make = editor.make();
93    let label = make.lifetime("'l");
94    let elements = vec![make.whitespace(" ").into(), label.syntax().clone().into()];
95    editor.insert_all(Position::after(token), elements);
96
97    if let Some(cap) = ctx.config.snippet_cap {
98        editor.add_annotation(label.syntax(), builder.make_placeholder_snippet(cap));
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use crate::tests::{check_assist, check_assist_not_applicable};
105
106    use super::*;
107
108    #[test]
109    fn add_label() {
110        check_assist(
111            add_label_to_loop,
112            r#"
113fn main() {
114    loop$0 {
115        break;
116        continue;
117    }
118}"#,
119            r#"
120fn main() {
121    ${1:'l}: loop {
122        break ${2:'l};
123        continue ${0:'l};
124    }
125}"#,
126        );
127    }
128
129    #[test]
130    fn add_label_to_while_expr() {
131        check_assist(
132            add_label_to_loop,
133            r#"
134fn main() {
135    while$0 true {
136        break;
137        continue;
138    }
139}"#,
140            r#"
141fn main() {
142    ${1:'l}: while true {
143        break ${2:'l};
144        continue ${0:'l};
145    }
146}"#,
147        );
148    }
149
150    #[test]
151    fn add_label_to_for_expr() {
152        check_assist(
153            add_label_to_loop,
154            r#"
155fn main() {
156    for$0 _ in 0..5 {
157        break;
158        continue;
159    }
160}"#,
161            r#"
162fn main() {
163    ${1:'l}: for _ in 0..5 {
164        break ${2:'l};
165        continue ${0:'l};
166    }
167}"#,
168        );
169    }
170
171    #[test]
172    fn add_label_to_outer_loop() {
173        check_assist(
174            add_label_to_loop,
175            r#"
176fn main() {
177    loop$0 {
178        break;
179        continue;
180        loop {
181            break;
182            continue;
183        }
184    }
185}"#,
186            r#"
187fn main() {
188    ${1:'l}: loop {
189        break ${2:'l};
190        continue ${0:'l};
191        loop {
192            break;
193            continue;
194        }
195    }
196}"#,
197        );
198    }
199
200    #[test]
201    fn add_label_to_inner_loop() {
202        check_assist(
203            add_label_to_loop,
204            r#"
205fn main() {
206    loop {
207        break;
208        continue;
209        loop$0 {
210            break;
211            continue;
212        }
213    }
214}"#,
215            r#"
216fn main() {
217    loop {
218        break;
219        continue;
220        ${1:'l}: loop {
221            break ${2:'l};
222            continue ${0:'l};
223        }
224    }
225}"#,
226        );
227    }
228
229    #[test]
230    fn do_not_add_label_if_exists() {
231        check_assist_not_applicable(
232            add_label_to_loop,
233            r#"
234fn main() {
235    'l: loop$0 {
236        break 'l;
237        continue 'l;
238    }
239}"#,
240        );
241    }
242
243    #[test]
244    fn do_not_add_label_if_outside_keyword() {
245        check_assist_not_applicable(
246            add_label_to_loop,
247            r#"
248fn main() {
249    'l: loop {$0
250        break 'l;
251        continue 'l;
252    }
253}"#,
254        );
255
256        check_assist_not_applicable(
257            add_label_to_loop,
258            r#"
259fn main() {
260    'l: while true {$0
261        break 'l;
262        continue 'l;
263    }
264}"#,
265        );
266    }
267}