ra_ap_syntax_bridge/
prettify_macro_expansion.rs

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