From 3ccd05fedc46796f793295901a8619492256468e Mon Sep 17 00:00:00 2001 From: Edwin Cheng Date: Mon, 18 Nov 2019 02:47:50 +0800 Subject: Add recursive expand in vscode --- crates/ra_ide_api/src/expand_macro.rs | 112 ++++++++++++++++++++++++++++++++++ crates/ra_ide_api/src/lib.rs | 5 ++ 2 files changed, 117 insertions(+) create mode 100644 crates/ra_ide_api/src/expand_macro.rs (limited to 'crates/ra_ide_api/src') 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..48dc90932 --- /dev/null +++ b/crates/ra_ide_api/src/expand_macro.rs @@ -0,0 +1,112 @@ +//! FIXME: write short doc here + +use crate::{db::RootDatabase, FilePosition}; +use ra_db::SourceDatabase; +use rustc_hash::FxHashMap; + +use hir::db::AstDatabase; +use ra_syntax::{ + algo::{find_node_at_offset, replace_descendants}, + ast::{self}, + AstNode, NodeOrToken, SyntaxKind, SyntaxNode, WalkEvent, +}; + +fn insert_whitespaces(syn: SyntaxNode) -> String { + let mut res = String::new(); + + let mut token_iter = syn + .preorder_with_tokens() + .filter_map(|event| { + if let WalkEvent::Enter(NodeOrToken::Token(token)) = event { + Some(token) + } else { + None + } + }) + .peekable(); + + while let Some(token) = token_iter.next() { + res += &token.text().to_string(); + if token.kind().is_keyword() + || token.kind().is_literal() + || token.kind() == SyntaxKind::IDENT + { + if !token_iter.peek().map(|it| it.kind().is_punct()).unwrap_or(false) { + res += " "; + } + } + } + + res +} + +fn expand_macro_recur( + db: &RootDatabase, + source: hir::Source<&SyntaxNode>, + macro_call: &ast::MacroCall, +) -> Option { + let analyzer = hir::SourceAnalyzer::new(db, source, None); + let expansion = analyzer.expand(db, ¯o_call)?; + let expanded: SyntaxNode = db.parse_or_expand(expansion.file_id())?; + + let children = expanded.descendants().filter_map(ast::MacroCall::cast); + let mut replaces = FxHashMap::default(); + + for child in children.into_iter() { + let source = hir::Source::new(expansion.file_id(), source.ast); + let new_node = expand_macro_recur(db, source, &child)?; + + replaces.insert(child.syntax().clone().into(), new_node.into()); + } + + Some(replace_descendants(&expanded, &replaces)) +} + +pub(crate) fn expand_macro(db: &RootDatabase, position: FilePosition) -> Option<(String, String)> { + let parse = db.parse(position.file_id); + let file = parse.tree(); + let name_ref = find_node_at_offset::(file.syntax(), position.offset)?; + let mac = name_ref.syntax().ancestors().find_map(ast::MacroCall::cast)?; + + let source = hir::Source::new(position.file_id.into(), mac.syntax()); + + let expanded = expand_macro_recur(db, source, &mac)?; + + // FIXME: + // macro expansion may lose all white space information + // But we hope someday we can use ra_fmt for that + let res = insert_whitespaces(expanded); + Some((name_ref.text().to_string(), res)) +} + +#[cfg(test)] +mod tests { + use crate::mock_analysis::analysis_and_position; + + fn check_expand_macro(fixture: &str, expected: (&str, &str)) { + let (analysis, pos) = analysis_and_position(fixture); + + let result = analysis.expand_macro(pos).unwrap().unwrap(); + assert_eq!(result, (expected.0.to_string(), expected.1.to_string())); + } + + #[test] + fn macro_expand_recursive_expansion() { + check_expand_macro( + r#" + //- /lib.rs + macro_rules! bar { + () => { fn b() {} } + } + macro_rules! foo { + () => { bar!(); } + } + macro_rules! baz { + () => { foo!(); } + } + f<|>oo!(); + "#, + ("foo", "fn b(){}"), + ); + } +} diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 110ddcd62..d1b73ef6f 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs @@ -42,6 +42,7 @@ mod display; mod inlay_hints; mod wasm_shims; mod expand; +mod expand_macro; #[cfg(test)] mod marks; @@ -296,6 +297,10 @@ impl Analysis { self.with_db(|db| syntax_tree::syntax_tree(&db, file_id, text_range)) } + pub fn expand_macro(&self, position: FilePosition) -> Cancelable> { + self.with_db(|db| expand_macro::expand_macro(db, position)) + } + /// Returns an edit to remove all newlines in the range, cleaning up minor /// stuff like trailing commas. pub fn join_lines(&self, frange: FileRange) -> Cancelable { -- cgit v1.2.3