diff options
Diffstat (limited to 'crates/ra_ide_api/src')
-rw-r--r-- | crates/ra_ide_api/src/expand_macro.rs | 178 | ||||
-rw-r--r-- | crates/ra_ide_api/src/lib.rs | 6 |
2 files changed, 184 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 | |||
3 | use crate::{db::RootDatabase, FilePosition}; | ||
4 | use hir::db::AstDatabase; | ||
5 | use ra_db::SourceDatabase; | ||
6 | use rustc_hash::FxHashMap; | ||
7 | |||
8 | use ra_syntax::{ | ||
9 | algo::{find_node_at_offset, replace_descendants}, | ||
10 | ast::{self}, | ||
11 | AstNode, NodeOrToken, SyntaxKind, SyntaxNode, WalkEvent, T, | ||
12 | }; | ||
13 | |||
14 | pub struct ExpandedMacro { | ||
15 | pub name: String, | ||
16 | pub expansion: String, | ||
17 | } | ||
18 | |||
19 | pub(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 | |||
35 | fn 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, ¯o_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. | ||
60 | fn 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)] | ||
119 | mod 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###" | ||
149 | fn 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###" | ||
172 | fn some_thing() -> u32 { | ||
173 | let a = 0; | ||
174 | a+10 | ||
175 | } | ||
176 | "###); | ||
177 | } | ||
178 | } | ||
diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 110ddcd62..57ed97147 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs | |||
@@ -42,6 +42,7 @@ mod display; | |||
42 | mod inlay_hints; | 42 | mod inlay_hints; |
43 | mod wasm_shims; | 43 | mod wasm_shims; |
44 | mod expand; | 44 | mod expand; |
45 | mod expand_macro; | ||
45 | 46 | ||
46 | #[cfg(test)] | 47 | #[cfg(test)] |
47 | mod marks; | 48 | mod marks; |
@@ -65,6 +66,7 @@ pub use crate::{ | |||
65 | completion::{CompletionItem, CompletionItemKind, InsertTextFormat}, | 66 | completion::{CompletionItem, CompletionItemKind, InsertTextFormat}, |
66 | diagnostics::Severity, | 67 | diagnostics::Severity, |
67 | display::{file_structure, FunctionSignature, NavigationTarget, StructureNode}, | 68 | display::{file_structure, FunctionSignature, NavigationTarget, StructureNode}, |
69 | expand_macro::ExpandedMacro, | ||
68 | feature_flags::FeatureFlags, | 70 | feature_flags::FeatureFlags, |
69 | folding_ranges::{Fold, FoldKind}, | 71 | folding_ranges::{Fold, FoldKind}, |
70 | hover::HoverResult, | 72 | hover::HoverResult, |
@@ -296,6 +298,10 @@ impl Analysis { | |||
296 | self.with_db(|db| syntax_tree::syntax_tree(&db, file_id, text_range)) | 298 | self.with_db(|db| syntax_tree::syntax_tree(&db, file_id, text_range)) |
297 | } | 299 | } |
298 | 300 | ||
301 | pub fn expand_macro(&self, position: FilePosition) -> Cancelable<Option<ExpandedMacro>> { | ||
302 | self.with_db(|db| expand_macro::expand_macro(db, position)) | ||
303 | } | ||
304 | |||
299 | /// Returns an edit to remove all newlines in the range, cleaning up minor | 305 | /// Returns an edit to remove all newlines in the range, cleaning up minor |
300 | /// stuff like trailing commas. | 306 | /// stuff like trailing commas. |
301 | pub fn join_lines(&self, frange: FileRange) -> Cancelable<SourceChange> { | 307 | pub fn join_lines(&self, frange: FileRange) -> Cancelable<SourceChange> { |