1use ide_db::assists::AssistId;
2use syntax::{
3 AstNode, SyntaxKind, SyntaxToken, T,
4 algo::{previous_non_trivia_token, skip_trivia_token},
5 ast,
6};
7
8use crate::{AssistContext, Assists};
9
10pub(crate) fn toggle_macro_delimiter(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
30 #[derive(Debug)]
31 enum MacroDelims {
32 LPar,
33 RPar,
34 LBra,
35 RBra,
36 LCur,
37 RCur,
38 }
39
40 let token_tree = ctx.find_node_at_offset::<ast::TokenTree>()?;
41
42 let cursor_offset = ctx.offset();
43 let semicolon = macro_semicolon(&token_tree);
44
45 let ltoken = token_tree.left_delimiter_token()?;
46 let rtoken = token_tree.right_delimiter_token()?;
47
48 if is_macro_call(&token_tree) != Some(true) {
49 cov_mark::hit!(toggle_macro_delimiter_is_not_macro_call);
50 return None;
51 }
52
53 if !ltoken.text_range().contains(cursor_offset) && !rtoken.text_range().contains(cursor_offset)
54 {
55 return None;
56 }
57
58 let token = match ltoken.kind() {
59 T!['{'] => MacroDelims::LCur,
60 T!['('] => MacroDelims::LPar,
61 T!['['] => MacroDelims::LBra,
62 T!['}'] => MacroDelims::RBra,
63 T![')'] => MacroDelims::RPar,
64 T!['}'] => MacroDelims::RCur,
65 _ => return None,
66 };
67
68 acc.add(
69 AssistId::refactor("toggle_macro_delimiter"),
70 match token {
71 MacroDelims::LPar | MacroDelims::RPar => "Replace delimiters with braces",
72 MacroDelims::LBra | MacroDelims::RBra => "Replace delimiters with parentheses",
73 MacroDelims::LCur | MacroDelims::RCur => "Replace delimiters with brackets",
74 },
75 token_tree.syntax().text_range(),
76 |builder| {
77 let editor = builder.make_editor(token_tree.syntax());
78 let make = editor.make();
79
80 match token {
81 MacroDelims::LPar | MacroDelims::RPar => {
82 editor.replace(ltoken, make.token(T!['{']));
83 editor.replace(rtoken, make.token(T!['}']));
84 if let Some(sc) = semicolon {
85 editor.delete(sc);
86 }
87 }
88 MacroDelims::LBra | MacroDelims::RBra => {
89 editor.replace(ltoken, make.token(T!['(']));
90 editor.replace(rtoken, make.token(T![')']));
91 }
92 MacroDelims::LCur | MacroDelims::RCur => {
93 editor.replace(ltoken, make.token(T!['[']));
94 if semicolon.is_some() || !needs_semicolon(token_tree) {
95 editor.replace(rtoken, make.token(T![']']));
96 } else {
97 editor.replace_with_many(
98 rtoken,
99 vec![make.token(T![']']).into(), make.token(T![;]).into()],
100 );
101 }
102 }
103 }
104 builder.add_file_edits(ctx.vfs_file_id(), editor);
105 },
106 )
107}
108
109fn is_macro_call(token_tree: &ast::TokenTree) -> Option<bool> {
110 let parent = token_tree.syntax().parent()?;
111 if ast::MacroCall::can_cast(parent.kind()) {
112 return Some(true);
113 }
114
115 let prev = previous_non_trivia_token(token_tree.syntax().clone())?;
116 let prev_prev = previous_non_trivia_token(prev.clone())?;
117 Some(prev.kind() == T![!] && prev_prev.kind() == SyntaxKind::IDENT)
118}
119
120fn macro_semicolon(token_tree: &ast::TokenTree) -> Option<SyntaxToken> {
121 let next_token = token_tree.syntax().last_token()?.next_token()?;
122 skip_trivia_token(next_token, syntax::Direction::Next).filter(|it| it.kind() == T![;])
123}
124
125fn needs_semicolon(tt: ast::TokenTree) -> bool {
126 (|| {
127 let call = ast::MacroCall::cast(tt.syntax().parent()?)?;
128 let container = call.syntax().parent()?;
129 let kind = container.kind();
130
131 if call.semicolon_token().is_some() {
132 return Some(false);
133 }
134
135 Some(
136 ast::ItemList::can_cast(kind)
137 || ast::SourceFile::can_cast(kind)
138 || ast::AssocItemList::can_cast(kind)
139 || ast::ExternItemList::can_cast(kind)
140 || ast::MacroItems::can_cast(kind)
141 || ast::MacroExpr::can_cast(kind)
142 && ast::ExprStmt::cast(container.parent()?)
143 .is_some_and(|it| it.semicolon_token().is_none()),
144 )
145 })()
146 .unwrap_or(false)
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 test_par() {
157 check_assist(
158 toggle_macro_delimiter,
159 r#"
160macro_rules! sth {
161 () => {};
162}
163
164sth!$0( );
165 "#,
166 r#"
167macro_rules! sth {
168 () => {};
169}
170
171sth!{ }
172 "#,
173 );
174
175 check_assist(
176 toggle_macro_delimiter,
177 r#"
178macro_rules! sth {
179 () => {};
180}
181
182fn foo() {
183 sth!$0( );
184}
185 "#,
186 r#"
187macro_rules! sth {
188 () => {};
189}
190
191fn foo() {
192 sth!{ }
193}
194 "#,
195 );
196 }
197
198 #[test]
199 fn test_braces() {
200 check_assist(
201 toggle_macro_delimiter,
202 r#"
203macro_rules! sth {
204 () => {};
205}
206
207sth!$0{ }
208 "#,
209 r#"
210macro_rules! sth {
211 () => {};
212}
213
214sth![ ];
215 "#,
216 );
217
218 check_assist(
219 toggle_macro_delimiter,
220 r#"
221macro_rules! sth {
222 () => {};
223}
224
225fn foo() -> i32 {
226 sth!$0{ }
227 2
228}
229 "#,
230 r#"
231macro_rules! sth {
232 () => {};
233}
234
235fn foo() -> i32 {
236 sth![ ];
237 2
238}
239 "#,
240 );
241
242 check_assist(
243 toggle_macro_delimiter,
244 r#"
245macro_rules! sth {
246 () => {2};
247}
248
249fn foo() {
250 sth!$0{ };
251}
252 "#,
253 r#"
254macro_rules! sth {
255 () => {2};
256}
257
258fn foo() {
259 sth![ ];
260}
261 "#,
262 );
263
264 check_assist(
265 toggle_macro_delimiter,
266 r#"
267macro_rules! sth {
268 () => {2};
269}
270
271fn foo() -> i32 {
272 sth!$0{ }
273}
274 "#,
275 r#"
276macro_rules! sth {
277 () => {2};
278}
279
280fn foo() -> i32 {
281 sth![ ]
282}
283 "#,
284 );
285
286 check_assist(
287 toggle_macro_delimiter,
288 r#"
289macro_rules! sth {
290 () => {};
291}
292impl () {
293 sth!$0{}
294}
295 "#,
296 r#"
297macro_rules! sth {
298 () => {};
299}
300impl () {
301 sth![];
302}
303 "#,
304 );
305
306 check_assist(
307 toggle_macro_delimiter,
308 r#"
309macro_rules! sth {
310 () => {2};
311}
312
313fn foo() -> i32 {
314 bar(sth!$0{ })
315}
316 "#,
317 r#"
318macro_rules! sth {
319 () => {2};
320}
321
322fn foo() -> i32 {
323 bar(sth![ ])
324}
325 "#,
326 );
327 }
328
329 #[test]
330 fn test_brackets() {
331 check_assist(
332 toggle_macro_delimiter,
333 r#"
334macro_rules! sth {
335 () => {};
336}
337
338sth!$0[ ];
339 "#,
340 r#"
341macro_rules! sth {
342 () => {};
343}
344
345sth!( );
346 "#,
347 )
348 }
349
350 #[test]
351 fn test_indent() {
352 check_assist(
353 toggle_macro_delimiter,
354 r#"
355mod abc {
356 macro_rules! sth {
357 () => {};
358 }
359
360 sth!$0{ }
361}
362 "#,
363 r#"
364mod abc {
365 macro_rules! sth {
366 () => {};
367 }
368
369 sth![ ];
370}
371 "#,
372 )
373 }
374
375 #[test]
376 fn test_unrelated_par() {
377 cov_mark::check!(toggle_macro_delimiter_is_not_macro_call);
378 check_assist_not_applicable(
379 toggle_macro_delimiter,
380 r#"
381macro_rules! prt {
382 ($e:expr) => {{
383 println!("{}", stringify!{$e});
384 }};
385}
386
387prt!((3 + 5$0));
388 "#,
389 )
390 }
391
392 #[test]
393 fn test_longer_macros() {
394 check_assist(
395 toggle_macro_delimiter,
396 r#"
397macro_rules! prt {
398 ($e:expr) => {{
399 println!("{}", stringify!{$e});
400 }};
401}
402
403prt!$0((3 + 5));
404"#,
405 r#"
406macro_rules! prt {
407 ($e:expr) => {{
408 println!("{}", stringify!{$e});
409 }};
410}
411
412prt!{(3 + 5)}
413"#,
414 )
415 }
416
417 #[test]
418 fn test_nested_macros() {
419 check_assist(
420 toggle_macro_delimiter,
421 r#"
422macro_rules! prt {
423 ($e:expr) => {{
424 println!("{}", stringify!{$e});
425 }};
426}
427
428macro_rules! abc {
429 ($e:expr) => {{
430 println!("{}", stringify!{$e});
431 }};
432}
433
434prt!{abc!$0(3 + 5)};
435"#,
436 r#"
437macro_rules! prt {
438 ($e:expr) => {{
439 println!("{}", stringify!{$e});
440 }};
441}
442
443macro_rules! abc {
444 ($e:expr) => {{
445 println!("{}", stringify!{$e});
446 }};
447}
448
449prt!{abc!{3 + 5}};
450"#,
451 )
452 }
453}