diff --git a/Cargo.lock b/Cargo.lock index cea85409369..78ad6dc07db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -611,6 +611,7 @@ dependencies = [ "syntax", "test_utils", "text_edit", + "toolchain", "tracing", "url", ] diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml index cd11b2801d7..d09b4a0dafe 100644 --- a/crates/ide/Cargo.toml +++ b/crates/ide/Cargo.toml @@ -37,6 +37,9 @@ ide_completion = { path = "../ide_completion", version = "0.0.0" } # something from some `hir_xxx` subpackage, reexport the API via `hir`. hir = { path = "../hir", version = "0.0.0" } +[target.'cfg(not(any(target_arch = "wasm32", target_os = "emscripten")))'.dependencies] +toolchain = { path = "../toolchain", version = "0.0.0" } + [dev-dependencies] test_utils = { path = "../test_utils" } expect-test = "1.2.2" diff --git a/crates/ide/src/expand_macro.rs b/crates/ide/src/expand_macro.rs index b4608d63c6c..e4061016e60 100644 --- a/crates/ide/src/expand_macro.rs +++ b/crates/ide/src/expand_macro.rs @@ -1,7 +1,7 @@ use hir::Semantics; use ide_db::{ - helpers::pick_best_token, syntax_helpers::insert_whitespace_into_node::insert_ws_into, - RootDatabase, + base_db::FileId, helpers::pick_best_token, + syntax_helpers::insert_whitespace_into_node::insert_ws_into, RootDatabase, }; use syntax::{ast, ted, AstNode, NodeOrToken, SyntaxKind, SyntaxNode, T}; @@ -58,10 +58,9 @@ pub(crate) fn expand_macro(db: &RootDatabase, position: FilePosition) -> Option< .take_while(|it| it != &token) .filter(|it| it.kind() == T![,]) .count(); - Some(ExpandedMacro { - name, - expansion: expansions.get(idx).cloned().map(insert_ws_into)?.to_string(), - }) + let expansion = + format(db, SyntaxKind::MACRO_ITEMS, position.file_id, expansions.get(idx).cloned()?); + Some(ExpandedMacro { name, expansion }) }); if derive.is_some() { @@ -70,28 +69,34 @@ pub(crate) fn expand_macro(db: &RootDatabase, position: FilePosition) -> Option< // FIXME: Intermix attribute and bang! expansions // currently we only recursively expand one of the two types - let mut expanded = None; - let mut name = None; - for node in tok.ancestors() { + let mut anc = tok.ancestors(); + let (name, expanded, kind) = loop { + let node = anc.next()?; + if let Some(item) = ast::Item::cast(node.clone()) { if let Some(def) = sema.resolve_attr_macro_call(&item) { - name = Some(def.name(db).to_string()); - expanded = expand_attr_macro_recur(&sema, &item); - break; + break ( + def.name(db).to_string(), + expand_attr_macro_recur(&sema, &item)?, + SyntaxKind::MACRO_ITEMS, + ); } } if let Some(mac) = ast::MacroCall::cast(node) { - name = Some(mac.path()?.segment()?.name_ref()?.to_string()); - expanded = expand_macro_recur(&sema, &mac); - break; + break ( + mac.path()?.segment()?.name_ref()?.to_string(), + expand_macro_recur(&sema, &mac)?, + mac.syntax().parent().map(|it| it.kind()).unwrap_or(SyntaxKind::MACRO_ITEMS), + ); } - } + }; // FIXME: // macro expansion may lose all white space information // But we hope someday we can use ra_fmt for that - let expansion = insert_ws_into(expanded?).to_string(); - Some(ExpandedMacro { name: name.unwrap_or_else(|| "???".to_owned()), expansion }) + let expansion = format(db, kind, position.file_id, expanded); + + Some(ExpandedMacro { name, expansion }) } fn expand_macro_recur( @@ -130,6 +135,77 @@ fn expand( Some(expanded) } +fn format(db: &RootDatabase, kind: SyntaxKind, file_id: FileId, expanded: SyntaxNode) -> String { + let expansion = insert_ws_into(expanded).to_string(); + + _format(db, kind, file_id, &expansion).unwrap_or(expansion) +} + +#[cfg(any(test, target_arch = "wasm32", target_os = "emscripten"))] +fn _format( + _db: &RootDatabase, + _kind: SyntaxKind, + _file_id: FileId, + _expansion: &str, +) -> Option { + None +} + +#[cfg(not(any(test, target_arch = "wasm32", target_os = "emscripten")))] +fn _format( + db: &RootDatabase, + kind: SyntaxKind, + file_id: FileId, + expansion: &str, +) -> Option { + use ide_db::base_db::{FileLoader, SourceDatabase}; + // hack until we get hygiene working (same character amount to preserve formatting as much as possible) + const DOLLAR_CRATE_REPLACE: &str = &"__r_a_"; + let expansion = expansion.replace("$crate", DOLLAR_CRATE_REPLACE); + let (prefix, suffix) = match kind { + SyntaxKind::MACRO_PAT => ("fn __(", ": u32);"), + SyntaxKind::MACRO_EXPR | SyntaxKind::MACRO_STMTS => ("fn __() {", "}"), + SyntaxKind::MACRO_TYPE => ("type __ =", ";"), + _ => ("", ""), + }; + let expansion = format!("{prefix}{expansion}{suffix}"); + + let &crate_id = db.relevant_crates(file_id).iter().next()?; + let edition = db.crate_graph()[crate_id].edition; + + let mut cmd = std::process::Command::new(toolchain::rustfmt()); + cmd.arg("--edition"); + cmd.arg(edition.to_string()); + + let mut rustfmt = cmd + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .ok()?; + + std::io::Write::write_all(&mut rustfmt.stdin.as_mut()?, expansion.as_bytes()).ok()?; + + let output = rustfmt.wait_with_output().ok()?; + let captured_stdout = String::from_utf8(output.stdout).ok()?; + + if output.status.success() && !captured_stdout.trim().is_empty() { + let output = captured_stdout.replace(DOLLAR_CRATE_REPLACE, "$crate"); + let output = output.trim().strip_prefix(prefix)?; + let output = match kind { + SyntaxKind::MACRO_PAT => { + output.strip_suffix(suffix).or_else(|| output.strip_suffix(": u32,\n);"))? + } + _ => output.strip_suffix(suffix)?, + }; + let trim_indent = stdx::trim_indent(output); + tracing::debug!("expand_macro: formatting succeeded"); + Some(trim_indent) + } else { + None + } +} + #[cfg(test)] mod tests { use expect_test::{expect, Expect}; diff --git a/crates/ide/src/navigation_target.rs b/crates/ide/src/navigation_target.rs index 55bb10d5e86..1fb3b5ec3f3 100644 --- a/crates/ide/src/navigation_target.rs +++ b/crates/ide/src/navigation_target.rs @@ -340,7 +340,6 @@ impl TryToNav for hir::Macro { Either::Left(it) => it, Either::Right(it) => it, }; - tracing::debug!("nav target {:#?}", name_owner.syntax()); let mut res = NavigationTarget::from_named( db, src.as_ref().with_value(name_owner),