ide_assists/handlers/
add_label_to_loop.rs1use 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
12pub(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}