aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api/src/expand_macro.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api/src/expand_macro.rs')
-rw-r--r--crates/ra_ide_api/src/expand_macro.rs178
1 files changed, 178 insertions, 0 deletions
diff --git a/crates/ra_ide_api/src/expand_macro.rs b/crates/ra_ide_api/src/expand_macro.rs
new file mode 100644
index 000000000..e9eb2a7fb
--- /dev/null
+++ b/crates/ra_ide_api/src/expand_macro.rs
@@ -0,0 +1,178 @@
1//! This modules implements "expand macro" functionality in the IDE
2
3use crate::{db::RootDatabase, FilePosition};
4use hir::db::AstDatabase;
5use ra_db::SourceDatabase;
6use rustc_hash::FxHashMap;
7
8use ra_syntax::{
9 algo::{find_node_at_offset, replace_descendants},
10 ast::{self},
11 AstNode, NodeOrToken, SyntaxKind, SyntaxNode, WalkEvent, T,
12};
13
14pub struct ExpandedMacro {
15 pub name: String,
16 pub expansion: String,
17}
18
19pub(crate) fn expand_macro(db: &RootDatabase, position: FilePosition) -> Option<ExpandedMacro> {
20 let parse = db.parse(position.file_id);
21 let file = parse.tree();
22 let name_ref = find_node_at_offset::<ast::NameRef>(file.syntax(), position.offset)?;
23 let mac = name_ref.syntax().ancestors().find_map(ast::MacroCall::cast)?;
24
25 let source = hir::Source::new(position.file_id.into(), mac.syntax());
26 let expanded = expand_macro_recur(db, source, &mac)?;
27
28 // FIXME:
29 // macro expansion may lose all white space information
30 // But we hope someday we can use ra_fmt for that
31 let expansion = insert_whitespaces(expanded);
32 Some(ExpandedMacro { name: name_ref.text().to_string(), expansion })
33}
34
35fn expand_macro_recur(
36 db: &RootDatabase,
37 source: hir::Source<&SyntaxNode>,
38 macro_call: &ast::MacroCall,
39) -> Option<SyntaxNode> {
40 let analyzer = hir::SourceAnalyzer::new(db, source, None);
41 let expansion = analyzer.expand(db, &macro_call)?;
42 let macro_file_id = expansion.file_id();
43 let expanded: SyntaxNode = db.parse_or_expand(macro_file_id)?;
44
45 let children = expanded.descendants().filter_map(ast::MacroCall::cast);
46 let mut replaces = FxHashMap::default();
47
48 for child in children.into_iter() {
49 let source = hir::Source::new(macro_file_id, source.ast);
50 let new_node = expand_macro_recur(db, source, &child)?;
51
52 replaces.insert(child.syntax().clone().into(), new_node.into());
53 }
54
55 Some(replace_descendants(&expanded, &replaces))
56}
57
58// FIXME: It would also be cool to share logic here and in the mbe tests,
59// which are pretty unreadable at the moment.
60fn insert_whitespaces(syn: SyntaxNode) -> String {
61 use SyntaxKind::*;
62
63 let mut res = String::new();
64 let mut token_iter = syn
65 .preorder_with_tokens()
66 .filter_map(|event| {
67 if let WalkEvent::Enter(NodeOrToken::Token(token)) = event {
68 Some(token)
69 } else {
70 None
71 }
72 })
73 .peekable();
74
75 let mut indent = 0;
76 let mut last: Option<SyntaxKind> = None;
77
78 while let Some(token) = token_iter.next() {
79 let mut is_next = |f: fn(SyntaxKind) -> bool, default| -> bool {
80 token_iter.peek().map(|it| f(it.kind())).unwrap_or(default)
81 };
82 let is_last = |f: fn(SyntaxKind) -> bool, default| -> bool {
83 last.map(|it| f(it)).unwrap_or(default)
84 };
85
86 res += &match token.kind() {
87 k @ _
88 if (k.is_keyword() || k.is_literal() || k == IDENT)
89 && is_next(|it| !it.is_punct(), true) =>
90 {
91 token.text().to_string() + " "
92 }
93 L_CURLY if is_next(|it| it != R_CURLY, true) => {
94 indent += 1;
95 format!(" {{\n{}", " ".repeat(indent))
96 }
97 R_CURLY if is_last(|it| it != L_CURLY, true) => {
98 indent = indent.checked_sub(1).unwrap_or(0);
99 format!("\n}}{}", " ".repeat(indent))
100 }
101 R_CURLY => {
102 indent = indent.checked_sub(1).unwrap_or(0);
103 format!("}}\n{}", " ".repeat(indent))
104 }
105 T![;] => format!(";\n{}", " ".repeat(indent)),
106 T![->] => " -> ".to_string(),
107 T![=] => " = ".to_string(),
108 T![=>] => " => ".to_string(),
109 _ => token.text().to_string(),
110 };
111
112 last = Some(token.kind());
113 }
114
115 res
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::mock_analysis::analysis_and_position;
122 use insta::assert_snapshot;
123
124 fn check_expand_macro(fixture: &str) -> ExpandedMacro {
125 let (analysis, pos) = analysis_and_position(fixture);
126 analysis.expand_macro(pos).unwrap().unwrap()
127 }
128
129 #[test]
130 fn macro_expand_recursive_expansion() {
131 let res = check_expand_macro(
132 r#"
133 //- /lib.rs
134 macro_rules! bar {
135 () => { fn b() {} }
136 }
137 macro_rules! foo {
138 () => { bar!(); }
139 }
140 macro_rules! baz {
141 () => { foo!(); }
142 }
143 f<|>oo!();
144 "#,
145 );
146
147 assert_eq!(res.name, "foo");
148 assert_snapshot!(res.expansion, @r###"
149fn b(){}
150"###);
151 }
152
153 #[test]
154 fn macro_expand_multiple_lines() {
155 let res = check_expand_macro(
156 r#"
157 //- /lib.rs
158 macro_rules! foo {
159 () => {
160 fn some_thing() -> u32 {
161 let a = 0;
162 a + 10
163 }
164 }
165 }
166 f<|>oo!();
167 "#,
168 );
169
170 assert_eq!(res.name, "foo");
171 assert_snapshot!(res.expansion, @r###"
172fn some_thing() -> u32 {
173 let a = 0;
174 a+10
175}
176"###);
177 }
178}