Skip to main content

ide_assists/handlers/
toggle_macro_delimiter.rs

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
10// Assist: toggle_macro_delimiter
11//
12// Change macro delimiters in the order of `( -> { -> [ -> (`.
13//
14// ```
15// macro_rules! sth {
16//     () => {};
17// }
18//
19// sth!$0( );
20// ```
21// ->
22// ```
23// macro_rules! sth {
24//     () => {};
25// }
26//
27// sth!{ }
28// ```
29pub(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}