diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b70d005fb9..5bc41533c4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,6 +5,7 @@ on: branches: - master - staging + - trying jobs: rust: diff --git a/Cargo.lock b/Cargo.lock index fe08edd29b..ef37351972 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1180,7 +1180,7 @@ dependencies = [ "once_cell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "ra_parser 0.1.0", "ra_text_edit 0.1.0", - "rowan 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rowan 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_lexer 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "smol_str 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1445,7 +1445,7 @@ dependencies = [ [[package]] name = "rowan" -version = "0.6.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1993,7 +1993,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum relative-path 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bedde000f40f2921ce439ea165c9c53fd629bfa115140c72e22aceacb4a21954" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" "checksum ron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2ece421e0c4129b90e4a35b6f625e472e96c552136f5093a2f4fa2bbb75a62d5" -"checksum rowan 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fc3a6fb2a35518af7cab43ec4e21ca82eb086a8b3bb1739e426dc3923d459607" +"checksum rowan 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d3a241900475bf2ba302061550ff50c82b45095ca95d23d1872345793fd42407" "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" "checksum rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7540fc8b0c49f096ee9c961cda096467dce8084bec6bdca2fc83895fd9b28cb8" "checksum rustc_lexer 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c86aae0c77166108c01305ee1a36a1e77289d7dc6ca0a3cd91ff4992de2d16a5" diff --git a/crates/ra_hir/src/source_binder.rs b/crates/ra_hir/src/source_binder.rs index f0ed8e2b25..5d3196c2a2 100644 --- a/crates/ra_hir/src/source_binder.rs +++ b/crates/ra_hir/src/source_binder.rs @@ -140,7 +140,7 @@ impl Expansion { exp_info.map_token_down(token) } - fn file_id(&self) -> HirFileId { + pub fn file_id(&self) -> HirFileId { self.macro_call_id.as_file(MacroFileKind::Items) } } diff --git a/crates/ra_hir_expand/src/quote.rs b/crates/ra_hir_expand/src/quote.rs index 35133d2164..65a35e52fa 100644 --- a/crates/ra_hir_expand/src/quote.rs +++ b/crates/ra_hir_expand/src/quote.rs @@ -172,12 +172,12 @@ impl_to_to_tokentrees! { u32 => self { tt::Literal{text: self.to_string().into()} }; usize => self { tt::Literal{text: self.to_string().into()}}; i32 => self { tt::Literal{text: self.to_string().into()}}; - &str => self { tt::Literal{text: self.to_string().into()}}; - String => self { tt::Literal{text: self.into()}}; tt::Leaf => self { self }; tt::Literal => self { self }; tt::Ident => self { self }; - tt::Punct => self { self } + tt::Punct => self { self }; + &str => self { tt::Literal{text: format!("{:?}", self.escape_default().to_string()).into()}}; + String => self { tt::Literal{text: format!("{:?}", self.escape_default().to_string()).into()}} } #[cfg(test)] @@ -200,7 +200,7 @@ mod tests { let a = 20; assert_eq!(quote!(#a).to_string(), "20"); let s: String = "hello".into(); - assert_eq!(quote!(#s).to_string(), "hello"); + assert_eq!(quote!(#s).to_string(), "\"hello\""); } fn mk_ident(name: &str) -> tt::Ident { 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 0000000000..e9eb2a7fb8 --- /dev/null +++ b/crates/ra_ide_api/src/expand_macro.rs @@ -0,0 +1,178 @@ +//! This modules implements "expand macro" functionality in the IDE + +use crate::{db::RootDatabase, FilePosition}; +use hir::db::AstDatabase; +use ra_db::SourceDatabase; +use rustc_hash::FxHashMap; + +use ra_syntax::{ + algo::{find_node_at_offset, replace_descendants}, + ast::{self}, + AstNode, NodeOrToken, SyntaxKind, SyntaxNode, WalkEvent, T, +}; + +pub struct ExpandedMacro { + pub name: String, + pub expansion: String, +} + +pub(crate) fn expand_macro(db: &RootDatabase, position: FilePosition) -> Option { + 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 expansion = insert_whitespaces(expanded); + Some(ExpandedMacro { name: name_ref.text().to_string(), expansion }) +} + +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 macro_file_id = expansion.file_id(); + let expanded: SyntaxNode = db.parse_or_expand(macro_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(macro_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)) +} + +// FIXME: It would also be cool to share logic here and in the mbe tests, +// which are pretty unreadable at the moment. +fn insert_whitespaces(syn: SyntaxNode) -> String { + use SyntaxKind::*; + + 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(); + + let mut indent = 0; + let mut last: Option = None; + + while let Some(token) = token_iter.next() { + let mut is_next = |f: fn(SyntaxKind) -> bool, default| -> bool { + token_iter.peek().map(|it| f(it.kind())).unwrap_or(default) + }; + let is_last = |f: fn(SyntaxKind) -> bool, default| -> bool { + last.map(|it| f(it)).unwrap_or(default) + }; + + res += &match token.kind() { + k @ _ + if (k.is_keyword() || k.is_literal() || k == IDENT) + && is_next(|it| !it.is_punct(), true) => + { + token.text().to_string() + " " + } + L_CURLY if is_next(|it| it != R_CURLY, true) => { + indent += 1; + format!(" {{\n{}", " ".repeat(indent)) + } + R_CURLY if is_last(|it| it != L_CURLY, true) => { + indent = indent.checked_sub(1).unwrap_or(0); + format!("\n}}{}", " ".repeat(indent)) + } + R_CURLY => { + indent = indent.checked_sub(1).unwrap_or(0); + format!("}}\n{}", " ".repeat(indent)) + } + T![;] => format!(";\n{}", " ".repeat(indent)), + T![->] => " -> ".to_string(), + T![=] => " = ".to_string(), + T![=>] => " => ".to_string(), + _ => token.text().to_string(), + }; + + last = Some(token.kind()); + } + + res +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock_analysis::analysis_and_position; + use insta::assert_snapshot; + + fn check_expand_macro(fixture: &str) -> ExpandedMacro { + let (analysis, pos) = analysis_and_position(fixture); + analysis.expand_macro(pos).unwrap().unwrap() + } + + #[test] + fn macro_expand_recursive_expansion() { + let res = check_expand_macro( + r#" + //- /lib.rs + macro_rules! bar { + () => { fn b() {} } + } + macro_rules! foo { + () => { bar!(); } + } + macro_rules! baz { + () => { foo!(); } + } + f<|>oo!(); + "#, + ); + + assert_eq!(res.name, "foo"); + assert_snapshot!(res.expansion, @r###" +fn b(){} +"###); + } + + #[test] + fn macro_expand_multiple_lines() { + let res = check_expand_macro( + r#" + //- /lib.rs + macro_rules! foo { + () => { + fn some_thing() -> u32 { + let a = 0; + a + 10 + } + } + } + f<|>oo!(); + "#, + ); + + assert_eq!(res.name, "foo"); + assert_snapshot!(res.expansion, @r###" +fn some_thing() -> u32 { + let a = 0; + a+10 +} +"###); + } +} diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 110ddcd626..57ed971470 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; @@ -65,6 +66,7 @@ pub use crate::{ completion::{CompletionItem, CompletionItemKind, InsertTextFormat}, diagnostics::Severity, display::{file_structure, FunctionSignature, NavigationTarget, StructureNode}, + expand_macro::ExpandedMacro, feature_flags::FeatureFlags, folding_ranges::{Fold, FoldKind}, hover::HoverResult, @@ -296,6 +298,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 { diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs index 379dab4384..f828efdee5 100644 --- a/crates/ra_lsp_server/src/main_loop.rs +++ b/crates/ra_lsp_server/src/main_loop.rs @@ -436,6 +436,7 @@ fn on_request( })? .on::(handlers::handle_analyzer_status)? .on::(handlers::handle_syntax_tree)? + .on::(handlers::handle_expand_macro)? .on::(handlers::handle_on_type_formatting)? .on::(handlers::handle_document_symbol)? .on::(handlers::handle_workspace_symbol)? diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index 20f9aee138..0461bf3854 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -47,6 +47,24 @@ pub fn handle_syntax_tree(world: WorldSnapshot, params: req::SyntaxTreeParams) - Ok(res) } +pub fn handle_expand_macro( + world: WorldSnapshot, + params: req::ExpandMacroParams, +) -> Result> { + let _p = profile("handle_expand_macro"); + let file_id = params.text_document.try_conv_with(&world)?; + let line_index = world.analysis().file_line_index(file_id)?; + let offset = params.position.map(|p| p.conv_with(&line_index)); + + match offset { + None => Ok(None), + Some(offset) => { + let res = world.analysis().expand_macro(FilePosition { file_id, offset })?; + Ok(res.map(|it| req::ExpandedMacro { name: it.name, expansion: it.expansion })) + } + } +} + pub fn handle_selection_range( world: WorldSnapshot, params: req::SelectionRangeParams, diff --git a/crates/ra_lsp_server/src/req.rs b/crates/ra_lsp_server/src/req.rs index d25fc57269..39361b7e8f 100644 --- a/crates/ra_lsp_server/src/req.rs +++ b/crates/ra_lsp_server/src/req.rs @@ -45,6 +45,28 @@ pub struct SyntaxTreeParams { pub range: Option, } +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExpandedMacro { + pub name: String, + pub expansion: String, +} + +pub enum ExpandMacro {} + +impl Request for ExpandMacro { + type Params = ExpandMacroParams; + type Result = Option; + const METHOD: &'static str = "rust-analyzer/expandMacro"; +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExpandMacroParams { + pub text_document: TextDocumentIdentifier, + pub position: Option, +} + pub enum SelectionRangeRequest {} impl Request for SelectionRangeRequest { diff --git a/crates/ra_syntax/Cargo.toml b/crates/ra_syntax/Cargo.toml index 45a18a73f7..5db2b58c0b 100644 --- a/crates/ra_syntax/Cargo.toml +++ b/crates/ra_syntax/Cargo.toml @@ -12,7 +12,7 @@ doctest = false [dependencies] itertools = "0.8.0" -rowan = "0.6.1" +rowan = "0.7.0" rustc_lexer = "0.1.0" rustc-hash = "1.0.1" arrayvec = "0.5.1" diff --git a/crates/ra_syntax/src/algo.rs b/crates/ra_syntax/src/algo.rs index 7cfea70f97..1c075082a4 100644 --- a/crates/ra_syntax/src/algo.rs +++ b/crates/ra_syntax/src/algo.rs @@ -134,23 +134,19 @@ pub fn insert_children( to_green_element(element) }); - let old_children = parent.green().children(); + let mut old_children = parent.green().children().map(|it| match it { + NodeOrToken::Token(it) => NodeOrToken::Token(it.clone()), + NodeOrToken::Node(it) => NodeOrToken::Node(it.clone()), + }); let new_children = match &position { - InsertPosition::First => { - to_insert.chain(old_children.iter().cloned()).collect::>() - } - InsertPosition::Last => old_children.iter().cloned().chain(to_insert).collect::>(), + InsertPosition::First => to_insert.chain(old_children).collect::>(), + InsertPosition::Last => old_children.chain(to_insert).collect::>(), InsertPosition::Before(anchor) | InsertPosition::After(anchor) => { let take_anchor = if let InsertPosition::After(_) = position { 1 } else { 0 }; let split_at = position_of_child(parent, anchor.clone()) + take_anchor; - let (before, after) = old_children.split_at(split_at); - before - .iter() - .cloned() - .chain(to_insert) - .chain(after.iter().cloned()) - .collect::>() + let before = old_children.by_ref().take(split_at).collect::>(); + before.into_iter().chain(to_insert).chain(old_children).collect::>() } }; @@ -168,13 +164,16 @@ pub fn replace_children( ) -> SyntaxNode { let start = position_of_child(parent, to_delete.start().clone()); let end = position_of_child(parent, to_delete.end().clone()); - let old_children = parent.green().children(); + let mut old_children = parent.green().children().map(|it| match it { + NodeOrToken::Token(it) => NodeOrToken::Token(it.clone()), + NodeOrToken::Node(it) => NodeOrToken::Node(it.clone()), + }); - let new_children = old_children[..start] - .iter() - .cloned() + let before = old_children.by_ref().take(start).collect::>(); + let new_children = before + .into_iter() .chain(to_insert.map(to_green_element)) - .chain(old_children[end + 1..].iter().cloned()) + .chain(old_children.skip(end + 1 - start)) .collect::>(); with_children(parent, new_children) } diff --git a/crates/ra_syntax/src/ast/extensions.rs b/crates/ra_syntax/src/ast/extensions.rs index 761b2435cc..4851dacb2e 100644 --- a/crates/ra_syntax/src/ast/extensions.rs +++ b/crates/ra_syntax/src/ast/extensions.rs @@ -32,7 +32,7 @@ impl ast::NameRef { } fn text_of_first_token(node: &SyntaxNode) -> &SmolStr { - node.green().children().first().and_then(|it| it.as_token()).unwrap().text() + node.green().children().next().and_then(|it| it.into_token()).unwrap().text() } impl ast::Attr { diff --git a/docs/user/features.md b/docs/user/features.md index c160dd70bd..309d2775d5 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -81,6 +81,10 @@ Join selected lines into one, smartly fixing up whitespace and trailing commas. Shows the parse tree of the current file. It exists mostly for debugging rust-analyzer itself. +#### Expand Macro Recursively + +Shows the full macro expansion of the macro at current cursor. + #### Status Shows internal statistic about memory usage of rust-analyzer diff --git a/editors/code/package.json b/editors/code/package.json index fbf675d461..e21dfa1745 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -91,6 +91,11 @@ "title": "Show Syntax Tree", "category": "Rust Analyzer" }, + { + "command": "rust-analyzer.expandMacro", + "title": "Expand macro recursively", + "category": "Rust Analyzer" + }, { "command": "rust-analyzer.matchingBrace", "title": "Find matching brace", diff --git a/editors/code/src/commands/expand_macro.ts b/editors/code/src/commands/expand_macro.ts new file mode 100644 index 0000000000..34e0c8fb33 --- /dev/null +++ b/editors/code/src/commands/expand_macro.ts @@ -0,0 +1,83 @@ +import * as vscode from 'vscode'; +import { Position, TextDocumentIdentifier } from 'vscode-languageclient'; +import { Server } from '../server'; + +export const expandMacroUri = vscode.Uri.parse( + 'rust-analyzer://expandMacro/[EXPANSION].rs' +); + +export class ExpandMacroContentProvider + implements vscode.TextDocumentContentProvider { + public eventEmitter = new vscode.EventEmitter(); + + public provideTextDocumentContent( + uri: vscode.Uri + ): vscode.ProviderResult { + async function handle() { + const editor = vscode.window.activeTextEditor; + if (editor == null) { + return ''; + } + + const position = editor.selection.active; + const request: MacroExpandParams = { + textDocument: { uri: editor.document.uri.toString() }, + position + }; + const expanded = await Server.client.sendRequest( + 'rust-analyzer/expandMacro', + request + ); + + if (expanded == null) { + return 'Not available'; + } + + return code_format(expanded); + } + + return handle(); + } + + get onDidChange(): vscode.Event { + return this.eventEmitter.event; + } +} + +// Opens the virtual file that will show the syntax tree +// +// The contents of the file come from the `TextDocumentContentProvider` +export function createHandle(provider: ExpandMacroContentProvider) { + return async () => { + const uri = expandMacroUri; + + const document = await vscode.workspace.openTextDocument(uri); + + provider.eventEmitter.fire(uri); + + return vscode.window.showTextDocument( + document, + vscode.ViewColumn.Two, + true + ); + }; +} + +interface MacroExpandParams { + textDocument: TextDocumentIdentifier; + position: Position; +} + +interface ExpandedMacro { + name: string; + expansion: string; +} + +function code_format(expanded: ExpandedMacro): string { + let result = `// Recursive expansion of ${expanded.name}! macro\n`; + result += '// ' + '='.repeat(result.length - 3); + result += '\n\n'; + result += expanded.expansion; + + return result; +} diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts index c194bd2eac..2ade6d331a 100644 --- a/editors/code/src/commands/index.ts +++ b/editors/code/src/commands/index.ts @@ -1,5 +1,6 @@ import * as analyzerStatus from './analyzer_status'; import * as applySourceChange from './apply_source_change'; +import * as expandMacro from './expand_macro'; import * as inlayHints from './inlay_hints'; import * as joinLines from './join_lines'; import * as matchingBrace from './matching_brace'; @@ -11,6 +12,7 @@ import * as syntaxTree from './syntaxTree'; export { analyzerStatus, applySourceChange, + expandMacro, joinLines, matchingBrace, parentModule, diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index c06928d122..683497dfd4 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -3,6 +3,7 @@ import * as lc from 'vscode-languageclient'; import * as commands from './commands'; import { CargoWatchProvider } from './commands/cargo_watch'; +import { ExpandMacroContentProvider } from './commands/expand_macro'; import { HintsUpdater } from './commands/inlay_hints'; import { interactivelyStartCargoWatch, @@ -97,6 +98,7 @@ export function activate(context: vscode.ExtensionContext) { ] ]; const syntaxTreeContentProvider = new SyntaxTreeContentProvider(); + const expandMacroContentProvider = new ExpandMacroContentProvider(); // The events below are plain old javascript events, triggered and handled by vscode vscode.window.onDidChangeActiveTextEditor( @@ -109,11 +111,21 @@ export function activate(context: vscode.ExtensionContext) { syntaxTreeContentProvider ) ); + disposeOnDeactivation( + vscode.workspace.registerTextDocumentContentProvider( + 'rust-analyzer', + expandMacroContentProvider + ) + ); registerCommand( 'rust-analyzer.syntaxTree', commands.syntaxTree.createHandle(syntaxTreeContentProvider) ); + registerCommand( + 'rust-analyzer.expandMacro', + commands.expandMacro.createHandle(expandMacroContentProvider) + ); vscode.workspace.onDidChangeTextDocument( events.changeTextDocument.createHandler(syntaxTreeContentProvider),