diff options
author | Ville Penttinen <[email protected]> | 2019-03-04 06:54:54 +0000 |
---|---|---|
committer | Ville Penttinen <[email protected]> | 2019-03-04 07:02:01 +0000 |
commit | 16ecd276f036de9b5dccdbcce55b25a2a5699385 (patch) | |
tree | 9e8e9ea50064a2987ca17a0e4bd5a75c80e2ab3e | |
parent | 0db95fc812d2c839e847527b774dfda170266cec (diff) |
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.
-rw-r--r-- | crates/ra_ide_api/src/lib.rs | 11 | ||||
-rw-r--r-- | crates/ra_ide_api/src/syntax_tree.rs | 85 | ||||
-rw-r--r-- | crates/ra_ide_api/tests/test/main.rs | 118 |
3 files changed, 206 insertions, 8 deletions
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; | |||
32 | mod impls; | 32 | mod impls; |
33 | mod assists; | 33 | mod assists; |
34 | mod diagnostics; | 34 | mod diagnostics; |
35 | mod syntax_tree; | ||
35 | 36 | ||
36 | #[cfg(test)] | 37 | #[cfg(test)] |
37 | mod marks; | 38 | mod marks; |
38 | 39 | ||
39 | use std::sync::Arc; | 40 | use std::sync::Arc; |
40 | 41 | ||
41 | use ra_syntax::{SourceFile, TreeArc, TextRange, TextUnit, AstNode, algo}; | 42 | use ra_syntax::{SourceFile, TreeArc, TextRange, TextUnit}; |
42 | use ra_text_edit::TextEdit; | 43 | use ra_text_edit::TextEdit; |
43 | use ra_db::{ | 44 | use ra_db::{ |
44 | SourceDatabase, CheckCanceled, | 45 | SourceDatabase, CheckCanceled, |
@@ -246,13 +247,7 @@ impl Analysis { | |||
246 | /// Returns a syntax tree represented as `String`, for debug purposes. | 247 | /// Returns a syntax tree represented as `String`, for debug purposes. |
247 | // FIXME: use a better name here. | 248 | // FIXME: use a better name here. |
248 | pub fn syntax_tree(&self, file_id: FileId, text_range: Option<TextRange>) -> String { | 249 | pub fn syntax_tree(&self, file_id: FileId, text_range: Option<TextRange>) -> String { |
249 | if let Some(text_range) = text_range { | 250 | syntax_tree::syntax_tree(&self.db, file_id, text_range) |
250 | let file = self.db.parse(file_id); | ||
251 | let node = algo::find_covering_node(file.syntax(), text_range); | ||
252 | node.debug_dump() | ||
253 | } else { | ||
254 | self.db.parse(file_id).syntax().debug_dump() | ||
255 | } | ||
256 | } | 251 | } |
257 | 252 | ||
258 | /// Returns an edit to remove all newlines in the range, cleaning up minor | 253 | /// 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 @@ | |||
1 | use ra_db::SourceDatabase; | ||
2 | use crate::db::RootDatabase; | ||
3 | use ra_syntax::{ | ||
4 | SourceFile, SyntaxNode, TextRange, AstNode, | ||
5 | algo::{self, visit::{visitor, Visitor}}, ast::{self, AstToken} | ||
6 | }; | ||
7 | |||
8 | pub use ra_db::FileId; | ||
9 | |||
10 | pub(crate) fn syntax_tree( | ||
11 | db: &RootDatabase, | ||
12 | file_id: FileId, | ||
13 | text_range: Option<TextRange>, | ||
14 | ) -> String { | ||
15 | if let Some(text_range) = text_range { | ||
16 | let file = db.parse(file_id); | ||
17 | let node = algo::find_covering_node(file.syntax(), text_range); | ||
18 | |||
19 | if let Some(tree) = syntax_tree_for_string(node, text_range) { | ||
20 | return tree; | ||
21 | } | ||
22 | |||
23 | node.debug_dump() | ||
24 | } else { | ||
25 | db.parse(file_id).syntax().debug_dump() | ||
26 | } | ||
27 | } | ||
28 | |||
29 | /// Attempts parsing the selected contents of a string literal | ||
30 | /// as rust syntax and returns its syntax tree | ||
31 | fn syntax_tree_for_string(node: &SyntaxNode, text_range: TextRange) -> Option<String> { | ||
32 | // When the range is inside a string | ||
33 | // we'll attempt parsing it as rust syntax | ||
34 | // to provide the syntax tree of the contents of the string | ||
35 | visitor() | ||
36 | .visit(|node: &ast::String| syntax_tree_for_token(node, text_range)) | ||
37 | .visit(|node: &ast::RawString| syntax_tree_for_token(node, text_range)) | ||
38 | .accept(node)? | ||
39 | } | ||
40 | |||
41 | fn syntax_tree_for_token<T: AstToken>(node: &T, text_range: TextRange) -> Option<String> { | ||
42 | // Range of the full node | ||
43 | let node_range = node.syntax().range(); | ||
44 | let text = node.text().to_string(); | ||
45 | |||
46 | // We start at some point inside the node | ||
47 | // Either we have selected the whole string | ||
48 | // or our selection is inside it | ||
49 | let start = text_range.start() - node_range.start(); | ||
50 | |||
51 | // how many characters we have selected | ||
52 | let len = text_range.len().to_usize(); | ||
53 | |||
54 | let node_len = node_range.len().to_usize(); | ||
55 | |||
56 | let start = start.to_usize(); | ||
57 | |||
58 | // We want to cap our length | ||
59 | let len = len.min(node_len); | ||
60 | |||
61 | // Ensure our slice is inside the actual string | ||
62 | let end = if start + len < text.len() { start + len } else { text.len() - start }; | ||
63 | |||
64 | let text = &text[start..end]; | ||
65 | |||
66 | // Remove possible extra string quotes from the start | ||
67 | // and the end of the string | ||
68 | let text = text | ||
69 | .trim_start_matches('r') | ||
70 | .trim_start_matches('#') | ||
71 | .trim_start_matches('"') | ||
72 | .trim_end_matches('#') | ||
73 | .trim_end_matches('"') | ||
74 | .trim(); | ||
75 | |||
76 | let parsed = SourceFile::parse(&text); | ||
77 | |||
78 | // If the "file" parsed without errors, | ||
79 | // return its syntax | ||
80 | if parsed.errors().is_empty() { | ||
81 | return Some(parsed.syntax().debug_dump()); | ||
82 | } | ||
83 | |||
84 | None | ||
85 | } | ||
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) | |||
272 | .trim() | 272 | .trim() |
273 | ); | 273 | ); |
274 | } | 274 | } |
275 | |||
276 | #[test] | ||
277 | fn test_syntax_tree_inside_string() { | ||
278 | let (analysis, range) = single_file_with_range( | ||
279 | r#"fn test() { | ||
280 | assert!(" | ||
281 | <|>fn foo() { | ||
282 | }<|> | ||
283 | fn bar() { | ||
284 | } | ||
285 | ", ""); | ||
286 | }"# | ||
287 | .trim(), | ||
288 | ); | ||
289 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)); | ||
290 | assert_eq!( | ||
291 | syn.trim(), | ||
292 | r#" | ||
293 | SOURCE_FILE@[0; 12) | ||
294 | FN_DEF@[0; 12) | ||
295 | FN_KW@[0; 2) | ||
296 | WHITESPACE@[2; 3) | ||
297 | NAME@[3; 6) | ||
298 | IDENT@[3; 6) "foo" | ||
299 | PARAM_LIST@[6; 8) | ||
300 | L_PAREN@[6; 7) | ||
301 | R_PAREN@[7; 8) | ||
302 | WHITESPACE@[8; 9) | ||
303 | BLOCK@[9; 12) | ||
304 | L_CURLY@[9; 10) | ||
305 | WHITESPACE@[10; 11) | ||
306 | R_CURLY@[11; 12) | ||
307 | "# | ||
308 | .trim() | ||
309 | ); | ||
310 | |||
311 | // With a raw string | ||
312 | let (analysis, range) = single_file_with_range( | ||
313 | r###"fn test() { | ||
314 | assert!(r#" | ||
315 | <|>fn foo() { | ||
316 | }<|> | ||
317 | fn bar() { | ||
318 | } | ||
319 | "#, ""); | ||
320 | }"### | ||
321 | .trim(), | ||
322 | ); | ||
323 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)); | ||
324 | assert_eq!( | ||
325 | syn.trim(), | ||
326 | r#" | ||
327 | SOURCE_FILE@[0; 12) | ||
328 | FN_DEF@[0; 12) | ||
329 | FN_KW@[0; 2) | ||
330 | WHITESPACE@[2; 3) | ||
331 | NAME@[3; 6) | ||
332 | IDENT@[3; 6) "foo" | ||
333 | PARAM_LIST@[6; 8) | ||
334 | L_PAREN@[6; 7) | ||
335 | R_PAREN@[7; 8) | ||
336 | WHITESPACE@[8; 9) | ||
337 | BLOCK@[9; 12) | ||
338 | L_CURLY@[9; 10) | ||
339 | WHITESPACE@[10; 11) | ||
340 | R_CURLY@[11; 12) | ||
341 | "# | ||
342 | .trim() | ||
343 | ); | ||
344 | |||
345 | // With a raw string | ||
346 | let (analysis, range) = single_file_with_range( | ||
347 | r###"fn test() { | ||
348 | assert!(r<|>#" | ||
349 | fn foo() { | ||
350 | } | ||
351 | fn bar() { | ||
352 | }"<|>#, ""); | ||
353 | }"### | ||
354 | .trim(), | ||
355 | ); | ||
356 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)); | ||
357 | assert_eq!( | ||
358 | syn.trim(), | ||
359 | r#" | ||
360 | SOURCE_FILE@[0; 25) | ||
361 | FN_DEF@[0; 12) | ||
362 | FN_KW@[0; 2) | ||
363 | WHITESPACE@[2; 3) | ||
364 | NAME@[3; 6) | ||
365 | IDENT@[3; 6) "foo" | ||
366 | PARAM_LIST@[6; 8) | ||
367 | L_PAREN@[6; 7) | ||
368 | R_PAREN@[7; 8) | ||
369 | WHITESPACE@[8; 9) | ||
370 | BLOCK@[9; 12) | ||
371 | L_CURLY@[9; 10) | ||
372 | WHITESPACE@[10; 11) | ||
373 | R_CURLY@[11; 12) | ||
374 | WHITESPACE@[12; 13) | ||
375 | FN_DEF@[13; 25) | ||
376 | FN_KW@[13; 15) | ||
377 | WHITESPACE@[15; 16) | ||
378 | NAME@[16; 19) | ||
379 | IDENT@[16; 19) "bar" | ||
380 | PARAM_LIST@[19; 21) | ||
381 | L_PAREN@[19; 20) | ||
382 | R_PAREN@[20; 21) | ||
383 | WHITESPACE@[21; 22) | ||
384 | BLOCK@[22; 25) | ||
385 | L_CURLY@[22; 23) | ||
386 | WHITESPACE@[23; 24) | ||
387 | R_CURLY@[24; 25) | ||
388 | |||
389 | "# | ||
390 | .trim() | ||
391 | ); | ||
392 | } | ||