From 16ecd276f036de9b5dccdbcce55b25a2a5699385 Mon Sep 17 00:00:00 2001 From: Ville Penttinen Date: Mon, 4 Mar 2019 08:54:54 +0200 Subject: Implement syntax tree support for syntax inside string This allows us to select a string or portions of it and try parsing it as rust syntax. This is mostly helpful when developing tests where the test itself contains some rust syntax as a string. --- crates/ra_ide_api/src/lib.rs | 11 +--- crates/ra_ide_api/src/syntax_tree.rs | 85 +++++++++++++++++++++++++ crates/ra_ide_api/tests/test/main.rs | 118 +++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 crates/ra_ide_api/src/syntax_tree.rs (limited to 'crates') diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index 3e7cfbb54..b8a4adbce 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs @@ -32,13 +32,14 @@ mod references; mod impls; mod assists; mod diagnostics; +mod syntax_tree; #[cfg(test)] mod marks; use std::sync::Arc; -use ra_syntax::{SourceFile, TreeArc, TextRange, TextUnit, AstNode, algo}; +use ra_syntax::{SourceFile, TreeArc, TextRange, TextUnit}; use ra_text_edit::TextEdit; use ra_db::{ SourceDatabase, CheckCanceled, @@ -246,13 +247,7 @@ impl Analysis { /// Returns a syntax tree represented as `String`, for debug purposes. // FIXME: use a better name here. pub fn syntax_tree(&self, file_id: FileId, text_range: Option) -> String { - if let Some(text_range) = text_range { - let file = self.db.parse(file_id); - let node = algo::find_covering_node(file.syntax(), text_range); - node.debug_dump() - } else { - self.db.parse(file_id).syntax().debug_dump() - } + syntax_tree::syntax_tree(&self.db, file_id, text_range) } /// Returns an edit to remove all newlines in the range, cleaning up minor diff --git a/crates/ra_ide_api/src/syntax_tree.rs b/crates/ra_ide_api/src/syntax_tree.rs new file mode 100644 index 000000000..cdee63d59 --- /dev/null +++ b/crates/ra_ide_api/src/syntax_tree.rs @@ -0,0 +1,85 @@ +use ra_db::SourceDatabase; +use crate::db::RootDatabase; +use ra_syntax::{ + SourceFile, SyntaxNode, TextRange, AstNode, + algo::{self, visit::{visitor, Visitor}}, ast::{self, AstToken} +}; + +pub use ra_db::FileId; + +pub(crate) fn syntax_tree( + db: &RootDatabase, + file_id: FileId, + text_range: Option, +) -> String { + if let Some(text_range) = text_range { + let file = db.parse(file_id); + let node = algo::find_covering_node(file.syntax(), text_range); + + if let Some(tree) = syntax_tree_for_string(node, text_range) { + return tree; + } + + node.debug_dump() + } else { + db.parse(file_id).syntax().debug_dump() + } +} + +/// Attempts parsing the selected contents of a string literal +/// as rust syntax and returns its syntax tree +fn syntax_tree_for_string(node: &SyntaxNode, 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 + visitor() + .visit(|node: &ast::String| syntax_tree_for_token(node, text_range)) + .visit(|node: &ast::RawString| syntax_tree_for_token(node, text_range)) + .accept(node)? +} + +fn syntax_tree_for_token(node: &T, text_range: TextRange) -> Option { + // Range of the full node + let node_range = node.syntax().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().to_usize(); + + let node_len = node_range.len().to_usize(); + + let start = start.to_usize(); + + // 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 < text.len() { start + len } else { text.len() - start }; + + let text = &text[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(); + + let parsed = SourceFile::parse(&text); + + // If the "file" parsed without errors, + // return its syntax + if parsed.errors().is_empty() { + return Some(parsed.syntax().debug_dump()); + } + + None +} diff --git a/crates/ra_ide_api/tests/test/main.rs b/crates/ra_ide_api/tests/test/main.rs index b0c80e255..0f0766f62 100644 --- a/crates/ra_ide_api/tests/test/main.rs +++ b/crates/ra_ide_api/tests/test/main.rs @@ -272,3 +272,121 @@ EXPR_STMT@[16; 58) .trim() ); } + +#[test] +fn test_syntax_tree_inside_string() { + let (analysis, range) = single_file_with_range( + r#"fn test() { + assert!(" +<|>fn foo() { +}<|> +fn bar() { +} + ", ""); +}"# + .trim(), + ); + let syn = analysis.syntax_tree(range.file_id, Some(range.range)); + assert_eq!( + syn.trim(), + r#" +SOURCE_FILE@[0; 12) + FN_DEF@[0; 12) + FN_KW@[0; 2) + 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@[9; 12) + L_CURLY@[9; 10) + WHITESPACE@[10; 11) + R_CURLY@[11; 12) +"# + .trim() + ); + + // With a raw string + let (analysis, range) = single_file_with_range( + r###"fn test() { + assert!(r#" +<|>fn foo() { +}<|> +fn bar() { +} + "#, ""); +}"### + .trim(), + ); + let syn = analysis.syntax_tree(range.file_id, Some(range.range)); + assert_eq!( + syn.trim(), + r#" +SOURCE_FILE@[0; 12) + FN_DEF@[0; 12) + FN_KW@[0; 2) + 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@[9; 12) + L_CURLY@[9; 10) + WHITESPACE@[10; 11) + R_CURLY@[11; 12) +"# + .trim() + ); + + // With a raw string + let (analysis, range) = single_file_with_range( + r###"fn test() { + assert!(r<|>#" +fn foo() { +} +fn bar() { +}"<|>#, ""); +}"### + .trim(), + ); + let syn = analysis.syntax_tree(range.file_id, Some(range.range)); + assert_eq!( + syn.trim(), + r#" +SOURCE_FILE@[0; 25) + FN_DEF@[0; 12) + FN_KW@[0; 2) + 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@[9; 12) + L_CURLY@[9; 10) + WHITESPACE@[10; 11) + R_CURLY@[11; 12) + WHITESPACE@[12; 13) + FN_DEF@[13; 25) + FN_KW@[13; 15) + 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@[22; 25) + L_CURLY@[22; 23) + WHITESPACE@[23; 24) + R_CURLY@[24; 25) + +"# + .trim() + ); +} -- cgit v1.2.3