From 1b0c7701cc97cd7bef8bb9729011d4cf291a60c5 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 13 Aug 2020 17:42:52 +0200 Subject: Rename ra_ide -> ide --- crates/ide/src/syntax_tree.rs | 359 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 crates/ide/src/syntax_tree.rs (limited to 'crates/ide/src/syntax_tree.rs') diff --git a/crates/ide/src/syntax_tree.rs b/crates/ide/src/syntax_tree.rs new file mode 100644 index 000000000..f80044959 --- /dev/null +++ b/crates/ide/src/syntax_tree.rs @@ -0,0 +1,359 @@ +use base_db::{FileId, SourceDatabase}; +use ide_db::RootDatabase; +use syntax::{ + algo, AstNode, NodeOrToken, SourceFile, + SyntaxKind::{RAW_STRING, STRING}, + SyntaxToken, TextRange, TextSize, +}; + +// Feature: Show Syntax Tree +// +// Shows the parse tree of the current file. It exists mostly for debugging +// rust-analyzer itself. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Show Syntax Tree** +// |=== +pub(crate) fn syntax_tree( + db: &RootDatabase, + file_id: FileId, + text_range: Option, +) -> String { + let parse = db.parse(file_id); + if let Some(text_range) = text_range { + let node = match algo::find_covering_element(parse.tree().syntax(), text_range) { + NodeOrToken::Node(node) => node, + NodeOrToken::Token(token) => { + if let Some(tree) = syntax_tree_for_string(&token, text_range) { + return tree; + } + token.parent() + } + }; + + format!("{:#?}", node) + } else { + format!("{:#?}", parse.tree().syntax()) + } +} + +/// Attempts parsing the selected contents of a string literal +/// as rust syntax and returns its syntax tree +fn syntax_tree_for_string(token: &SyntaxToken, text_range: TextRange) -> Option { + // When the range is inside a string + // we'll attempt parsing it as rust syntax + // to provide the syntax tree of the contents of the string + match token.kind() { + STRING | RAW_STRING => syntax_tree_for_token(token, text_range), + _ => None, + } +} + +fn syntax_tree_for_token(node: &SyntaxToken, text_range: TextRange) -> Option { + // Range of the full node + let node_range = node.text_range(); + let text = node.text().to_string(); + + // We start at some point inside the node + // Either we have selected the whole string + // or our selection is inside it + let start = text_range.start() - node_range.start(); + + // how many characters we have selected + let len = text_range.len(); + + let node_len = node_range.len(); + + let start = start; + + // We want to cap our length + let len = len.min(node_len); + + // Ensure our slice is inside the actual string + let end = + if start + len < TextSize::of(&text) { start + len } else { TextSize::of(&text) - start }; + + let text = &text[TextRange::new(start, end)]; + + // Remove possible extra string quotes from the start + // and the end of the string + let text = text + .trim_start_matches('r') + .trim_start_matches('#') + .trim_start_matches('"') + .trim_end_matches('#') + .trim_end_matches('"') + .trim() + // Remove custom markers + .replace("<|>", ""); + + let parsed = SourceFile::parse(&text); + + // If the "file" parsed without errors, + // return its syntax + if parsed.errors().is_empty() { + return Some(format!("{:#?}", parsed.tree().syntax())); + } + + None +} + +#[cfg(test)] +mod tests { + use test_utils::assert_eq_text; + + use crate::mock_analysis::{analysis_and_range, single_file}; + + #[test] + fn test_syntax_tree_without_range() { + // Basic syntax + let (analysis, file_id) = single_file(r#"fn foo() {}"#); + let syn = analysis.syntax_tree(file_id, None).unwrap(); + + assert_eq_text!( + syn.trim(), + r#" +SOURCE_FILE@0..11 + FN@0..11 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..11 + L_CURLY@9..10 "{" + R_CURLY@10..11 "}" +"# + .trim() + ); + + let (analysis, file_id) = single_file( + r#" +fn test() { + assert!(" + fn foo() { + } + ", ""); +}"# + .trim(), + ); + let syn = analysis.syntax_tree(file_id, None).unwrap(); + + assert_eq_text!( + syn.trim(), + r#" +SOURCE_FILE@0..60 + FN@0..60 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..7 + IDENT@3..7 "test" + PARAM_LIST@7..9 + L_PAREN@7..8 "(" + R_PAREN@8..9 ")" + WHITESPACE@9..10 " " + BLOCK_EXPR@10..60 + L_CURLY@10..11 "{" + WHITESPACE@11..16 "\n " + EXPR_STMT@16..58 + MACRO_CALL@16..57 + PATH@16..22 + PATH_SEGMENT@16..22 + NAME_REF@16..22 + IDENT@16..22 "assert" + BANG@22..23 "!" + TOKEN_TREE@23..57 + L_PAREN@23..24 "(" + STRING@24..52 "\"\n fn foo() {\n ..." + COMMA@52..53 "," + WHITESPACE@53..54 " " + STRING@54..56 "\"\"" + R_PAREN@56..57 ")" + SEMICOLON@57..58 ";" + WHITESPACE@58..59 "\n" + R_CURLY@59..60 "}" +"# + .trim() + ); + } + + #[test] + fn test_syntax_tree_with_range() { + let (analysis, range) = analysis_and_range(r#"<|>fn foo() {}<|>"#.trim()); + let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); + + assert_eq_text!( + syn.trim(), + r#" +FN@0..11 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..11 + L_CURLY@9..10 "{" + R_CURLY@10..11 "}" +"# + .trim() + ); + + let (analysis, range) = analysis_and_range( + r#"fn test() { + <|>assert!(" + fn foo() { + } + ", "");<|> +}"# + .trim(), + ); + let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); + + assert_eq_text!( + syn.trim(), + r#" +EXPR_STMT@16..58 + MACRO_CALL@16..57 + PATH@16..22 + PATH_SEGMENT@16..22 + NAME_REF@16..22 + IDENT@16..22 "assert" + BANG@22..23 "!" + TOKEN_TREE@23..57 + L_PAREN@23..24 "(" + STRING@24..52 "\"\n fn foo() {\n ..." + COMMA@52..53 "," + WHITESPACE@53..54 " " + STRING@54..56 "\"\"" + R_PAREN@56..57 ")" + SEMICOLON@57..58 ";" +"# + .trim() + ); + } + + #[test] + fn test_syntax_tree_inside_string() { + let (analysis, range) = analysis_and_range( + r#"fn test() { + assert!(" +<|>fn foo() { +}<|> +fn bar() { +} + ", ""); +}"# + .trim(), + ); + let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); + assert_eq_text!( + syn.trim(), + r#" +SOURCE_FILE@0..12 + FN@0..12 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..12 + L_CURLY@9..10 "{" + WHITESPACE@10..11 "\n" + R_CURLY@11..12 "}" +"# + .trim() + ); + + // With a raw string + let (analysis, range) = analysis_and_range( + r###"fn test() { + assert!(r#" +<|>fn foo() { +}<|> +fn bar() { +} + "#, ""); +}"### + .trim(), + ); + let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); + assert_eq_text!( + syn.trim(), + r#" +SOURCE_FILE@0..12 + FN@0..12 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..12 + L_CURLY@9..10 "{" + WHITESPACE@10..11 "\n" + R_CURLY@11..12 "}" +"# + .trim() + ); + + // With a raw string + let (analysis, range) = analysis_and_range( + r###"fn test() { + assert!(r<|>#" +fn foo() { +} +fn bar() { +}"<|>#, ""); +}"### + .trim(), + ); + let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); + assert_eq_text!( + syn.trim(), + r#" +SOURCE_FILE@0..25 + FN@0..12 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..12 + L_CURLY@9..10 "{" + WHITESPACE@10..11 "\n" + R_CURLY@11..12 "}" + WHITESPACE@12..13 "\n" + FN@13..25 + FN_KW@13..15 "fn" + WHITESPACE@15..16 " " + NAME@16..19 + IDENT@16..19 "bar" + PARAM_LIST@19..21 + L_PAREN@19..20 "(" + R_PAREN@20..21 ")" + WHITESPACE@21..22 " " + BLOCK_EXPR@22..25 + L_CURLY@22..23 "{" + WHITESPACE@23..24 "\n" + R_CURLY@24..25 "}" +"# + .trim() + ); + } +} -- cgit v1.2.3