ra_ap_syntax_bridge/
prettify_macro_expansion.rs

1//! Utilities for formatting macro expanded nodes until we get a proper formatter.
2use syntax::{
3    ast::make,
4    ted::{self, Position},
5    NodeOrToken,
6    SyntaxKind::{self, *},
7    SyntaxNode, SyntaxToken, WalkEvent, T,
8};
9
10/// Renders a [`SyntaxNode`] with whitespace inserted between tokens that require them.
11///
12/// This is an internal API that is only exported because `mbe` needs it for tests and cannot depend
13/// on `hir-expand`. For any purpose other than tests, you are supposed to use the `prettify_macro_expansion`
14/// from `hir-expand` that handles `$crate` for you.
15#[deprecated = "use `hir_expand::prettify_macro_expansion()` instead"]
16pub fn prettify_macro_expansion(
17    syn: SyntaxNode,
18    dollar_crate_replacement: &mut dyn FnMut(&SyntaxToken) -> SyntaxToken,
19) -> SyntaxNode {
20    let mut indent = 0;
21    let mut last: Option<SyntaxKind> = None;
22    let mut mods = Vec::new();
23    let mut dollar_crate_replacements = Vec::new();
24    let syn = syn.clone_subtree().clone_for_update();
25
26    let before = Position::before;
27    let after = Position::after;
28
29    let do_indent = |pos: fn(_) -> Position, token: &SyntaxToken, indent| {
30        (pos(token.clone()), make::tokens::whitespace(&" ".repeat(4 * indent)))
31    };
32    let do_ws = |pos: fn(_) -> Position, token: &SyntaxToken| {
33        (pos(token.clone()), make::tokens::single_space())
34    };
35    let do_nl = |pos: fn(_) -> Position, token: &SyntaxToken| {
36        (pos(token.clone()), make::tokens::single_newline())
37    };
38
39    for event in syn.preorder_with_tokens() {
40        let token = match event {
41            WalkEvent::Enter(NodeOrToken::Token(token)) => token,
42            WalkEvent::Leave(NodeOrToken::Node(node))
43                if matches!(
44                    node.kind(),
45                    ATTR | MATCH_ARM | STRUCT | ENUM | UNION | FN | IMPL | MACRO_RULES
46                ) =>
47            {
48                if indent > 0 {
49                    mods.push((
50                        Position::after(node.clone()),
51                        make::tokens::whitespace(&" ".repeat(4 * indent)),
52                    ));
53                }
54                if node.parent().is_some() {
55                    mods.push((Position::after(node), make::tokens::single_newline()));
56                }
57                continue;
58            }
59            _ => continue,
60        };
61        if token.kind() == SyntaxKind::IDENT && token.text() == "$crate" {
62            dollar_crate_replacements.push((token.clone(), dollar_crate_replacement(&token)));
63        }
64        let tok = &token;
65
66        let is_next = |f: fn(SyntaxKind) -> bool, default| -> bool {
67            tok.next_token().map(|it| f(it.kind())).unwrap_or(default)
68        };
69        let is_last =
70            |f: fn(SyntaxKind) -> bool, default| -> bool { last.map(f).unwrap_or(default) };
71
72        match tok.kind() {
73            k if is_text(k)
74                && is_next(|it| !it.is_punct() || matches!(it, T![_] | T![#]), false) =>
75            {
76                mods.push(do_ws(after, tok));
77            }
78            L_CURLY if is_next(|it| it != R_CURLY, true) => {
79                indent += 1;
80                if is_last(is_text, false) {
81                    mods.push(do_ws(before, tok));
82                }
83
84                mods.push(do_indent(after, tok, indent));
85                mods.push(do_nl(after, tok));
86            }
87            R_CURLY if is_last(|it| it != L_CURLY, true) => {
88                indent = indent.saturating_sub(1);
89
90                if indent > 0 {
91                    mods.push(do_indent(before, tok, indent));
92                }
93                mods.push(do_nl(before, tok));
94            }
95            R_CURLY => {
96                if indent > 0 {
97                    mods.push(do_indent(after, tok, indent));
98                }
99                mods.push(do_nl(after, tok));
100            }
101            LIFETIME_IDENT if is_next(is_text, true) => {
102                mods.push(do_ws(after, tok));
103            }
104            AS_KW | DYN_KW | IMPL_KW | CONST_KW | MUT_KW => {
105                mods.push(do_ws(after, tok));
106            }
107            T![;] if is_next(|it| it != R_CURLY, true) => {
108                if indent > 0 {
109                    mods.push(do_indent(after, tok, indent));
110                }
111                mods.push(do_nl(after, tok));
112            }
113            T![=] if is_next(|it| it == T![>], false) => {
114                // FIXME: this branch is for `=>` in macro_rules!, which is currently parsed as
115                // two separate symbols.
116                mods.push(do_ws(before, tok));
117                mods.push(do_ws(after, &tok.next_token().unwrap()));
118            }
119            T![->] | T![=] | T![=>] => {
120                mods.push(do_ws(before, tok));
121                mods.push(do_ws(after, tok));
122            }
123            T![!] if is_last(|it| it == MACRO_RULES_KW, false) && is_next(is_text, false) => {
124                mods.push(do_ws(after, tok));
125            }
126            _ => (),
127        }
128
129        last = Some(tok.kind());
130    }
131
132    for (pos, insert) in mods {
133        ted::insert(pos, insert);
134    }
135    for (old, new) in dollar_crate_replacements {
136        ted::replace(old, new);
137    }
138
139    if let Some(it) = syn.last_token().filter(|it| it.kind() == SyntaxKind::WHITESPACE) {
140        ted::remove(it);
141    }
142
143    syn
144}
145
146fn is_text(k: SyntaxKind) -> bool {
147    // Consider all keywords in all editions.
148    k.is_any_identifier() || k.is_literal() || k == UNDERSCORE
149}