Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--crates/ide/src/lib.rs12
-rw-r--r--crates/ide/src/syntax_tree.rs338
-rw-r--r--crates/ide/src/view_syntax_tree.rs226
-rw-r--r--crates/rust-analyzer/src/handlers/request.rs10
-rw-r--r--crates/rust-analyzer/src/lsp/ext.rs11
-rw-r--r--crates/rust-analyzer/src/main_loop.rs2
-rw-r--r--docs/dev/lsp-extensions.md19
-rw-r--r--editors/code/package.json105
-rw-r--r--editors/code/src/ast_inspector.ts216
-rw-r--r--editors/code/src/commands.ts167
-rw-r--r--editors/code/src/config.ts4
-rw-r--r--editors/code/src/ctx.ts93
-rw-r--r--editors/code/src/lsp_ext.ts4
-rw-r--r--editors/code/src/main.ts5
-rw-r--r--editors/code/src/syntax_tree_provider.ts301
15 files changed, 812 insertions, 701 deletions
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 6e7c718953..af9a7ce788 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -48,7 +48,6 @@ mod ssr;
mod static_index;
mod status;
mod syntax_highlighting;
-mod syntax_tree;
mod test_explorer;
mod typing;
mod view_crate_graph;
@@ -56,6 +55,7 @@ mod view_hir;
mod view_item_tree;
mod view_memory_layout;
mod view_mir;
+mod view_syntax_tree;
use std::{iter, panic::UnwindSafe};
@@ -329,14 +329,8 @@ 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<TextRange>,
- ) -> Cancellable<String> {
- self.with_db(|db| syntax_tree::syntax_tree(db, file_id, text_range))
+ pub fn view_syntax_tree(&self, file_id: FileId) -> Cancellable<String> {
+ self.with_db(|db| view_syntax_tree::view_syntax_tree(db, file_id))
}
pub fn view_hir(&self, position: FilePosition) -> Cancellable<String> {
diff --git a/crates/ide/src/syntax_tree.rs b/crates/ide/src/syntax_tree.rs
deleted file mode 100644
index e241cb82bd..0000000000
--- a/crates/ide/src/syntax_tree.rs
+++ /dev/null
@@ -1,338 +0,0 @@
-use hir::Semantics;
-use ide_db::{FileId, RootDatabase};
-use syntax::{
- AstNode, NodeOrToken, SourceFile, SyntaxKind::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**
-// |===
-// image::https://user-images.githubusercontent.com/48062697/113065586-068bdb80-91b1-11eb-9507-fee67f9f45a0.gif[]
-pub(crate) fn syntax_tree(
- db: &RootDatabase,
- file_id: FileId,
- text_range: Option<TextRange>,
-) -> String {
- let sema = Semantics::new(db);
- let parse = sema.parse_guess_edition(file_id);
- if let Some(text_range) = text_range {
- let node = match parse.syntax().covering_element(text_range) {
- NodeOrToken::Node(node) => node,
- NodeOrToken::Token(token) => {
- if let Some(tree) = syntax_tree_for_string(&token, text_range) {
- return tree;
- }
- token.parent().unwrap()
- }
- };
-
- format!("{node:#?}")
- } else {
- format!("{:#?}", parse.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<String> {
- // 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 => syntax_tree_for_token(token, text_range),
- _ => None,
- }
-}
-
-fn syntax_tree_for_token(node: &SyntaxToken, text_range: TextRange) -> Option<String> {
- // Range of the full node
- let node_range = node.text_range();
- let text = node.text().to_owned();
-
- // 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();
-
- // 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("$0", "");
-
- let parsed = SourceFile::parse(&text, span::Edition::CURRENT_FIXME);
-
- // 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 expect_test::expect;
-
- use crate::fixture;
-
- fn check(ra_fixture: &str, expect: expect_test::Expect) {
- let (analysis, file_id) = fixture::file(ra_fixture);
- let syn = analysis.syntax_tree(file_id, None).unwrap();
- expect.assert_eq(&syn)
- }
- fn check_range(ra_fixture: &str, expect: expect_test::Expect) {
- let (analysis, frange) = fixture::range(ra_fixture);
- let syn = analysis.syntax_tree(frange.file_id, Some(frange.range)).unwrap();
- expect.assert_eq(&syn)
- }
-
- #[test]
- fn test_syntax_tree_without_range() {
- // Basic syntax
- check(
- r#"fn foo() {}"#,
- expect![[r#"
- "#]],
- );
-
- check(
- r#"
-fn test() {
- assert!("
- fn foo() {
- }
- ", "");
-}"#,
- expect![[r#"
- [email protected] "\"\n fn foo() {\n ..."
- "#]],
- )
- }
-
- #[test]
- fn test_syntax_tree_with_range() {
- check_range(
- r#"$0fn foo() {}$0"#,
- expect![[r#"
- "#]],
- );
-
- check_range(
- r#"
-fn test() {
- $0assert!("
- fn foo() {
- }
- ", "");$0
-}"#,
- expect![[r#"
- [email protected] "\"\n fn foo() {\n ..."
- "#]],
- );
- }
-
- #[test]
- fn test_syntax_tree_inside_string() {
- check_range(
- r#"fn test() {
- assert!("
-$0fn foo() {
-}$0
-fn bar() {
-}
- ", "");
-}"#,
- expect![[r#"
- "#]],
- );
-
- // With a raw string
- check_range(
- r###"fn test() {
- assert!(r#"
-$0fn foo() {
-}$0
-fn bar() {
-}
- "#, "");
-}"###,
- expect![[r#"
- "#]],
- );
-
- // With a raw string
- check_range(
- r###"fn test() {
- assert!(r$0#"
-fn foo() {
-}
-fn bar() {
-}"$0#, "");
-}"###,
- expect![[r#"
- "#]],
- );
- }
-}
diff --git a/crates/ide/src/view_syntax_tree.rs b/crates/ide/src/view_syntax_tree.rs
new file mode 100644
index 0000000000..b2adff0f36
--- /dev/null
+++ b/crates/ide/src/view_syntax_tree.rs
@@ -0,0 +1,226 @@
+use hir::Semantics;
+use ide_db::{FileId, RootDatabase};
+use span::TextRange;
+use stdx::format_to;
+use syntax::{
+ ast::{self, IsString},
+ AstNode, AstToken, NodeOrToken, SourceFile, SyntaxNode, SyntaxToken, WalkEvent,
+};
+
+// Feature: Show Syntax Tree
+//
+// Shows a tree view with the syntax tree of the current file
+//
+// |===
+// | Editor | Panel Name
+//
+// | VS Code | **Rust Syntax Tree**
+// |===
+pub(crate) fn view_syntax_tree(db: &RootDatabase, file_id: FileId) -> String {
+ let sema = Semantics::new(db);
+ let parse = sema.parse_guess_edition(file_id);
+ syntax_node_to_json(parse.syntax(), None)
+}
+
+fn syntax_node_to_json(node: &SyntaxNode, ctx: Option<InStringCtx>) -> String {
+ let mut result = String::new();
+ for event in node.preorder_with_tokens() {
+ match event {
+ WalkEvent::Enter(it) => {
+ let kind = it.kind();
+ let (text_range, inner_range_str) = match &ctx {
+ Some(ctx) => {
+ let inner_start: u32 = it.text_range().start().into();
+ let inner_end: u32 = it.text_range().end().into();
+
+ let mut true_start = inner_start + ctx.offset;
+ let mut true_end = inner_end + ctx.offset;
+ for pos in &ctx.marker_positions {
+ if *pos >= inner_end {
+ break;
+ }
+
+ // We conditionally add to true_start in case
+ // the marker is between the start and end.
+ true_start += 2 * (*pos < inner_start) as u32;
+ true_end += 2;
+ }
+
+ let true_range = TextRange::new(true_start.into(), true_end.into());
+
+ (
+ true_range,
+ format!(
+ r#","istart":{:?},"iend":{:?}"#,
+ it.text_range().start(),
+ it.text_range().end()
+ ),
+ )
+ }
+ None => (it.text_range(), "".to_owned()),
+ };
+ let start = text_range.start();
+ let end = text_range.end();
+
+ match it {
+ NodeOrToken::Node(_) => {
+ format_to!(
+ result,
+ r#"{{"type":"Node","kind":"{kind:?}","start":{start:?},"end":{end:?}{inner_range_str},"children":["#
+ );
+ }
+ NodeOrToken::Token(token) => {
+ let comma = if token.next_sibling_or_token().is_some() { "," } else { "" };
+ match parse_rust_string(token) {
+ Some(parsed) => {
+ format_to!(
+ result,
+ r#"{{"type":"Node","kind":"{kind:?}","start":{start:?},"end":{end:?}{inner_range_str},"children":[{parsed}]}}{comma}"#
+ );
+ }
+ None => format_to!(
+ result,
+ r#"{{"type":"Token","kind":"{kind:?}","start":{start:?},"end":{end:?}{inner_range_str}}}{comma}"#
+ ),
+ }
+ }
+ }
+ }
+ WalkEvent::Leave(it) => match it {
+ NodeOrToken::Node(node) => {
+ let comma = if node.next_sibling_or_token().is_some() { "," } else { "" };
+ format_to!(result, "]}}{comma}")
+ }
+ NodeOrToken::Token(_) => (),
+ },
+ }
+ }
+
+ result
+}
+
+fn parse_rust_string(token: SyntaxToken) -> Option<String> {
+ let string_node = ast::String::cast(token)?;
+ let text = string_node.value().ok()?;
+
+ let mut trim_result = String::new();
+ let mut marker_positions = Vec::new();
+ let mut skipped = 0;
+ let mut last_end = 0;
+ for (start, part) in text.match_indices("$0") {
+ marker_positions.push((start - skipped) as u32);
+ trim_result.push_str(&text[last_end..start]);
+ skipped += part.len();
+ last_end = start + part.len();
+ }
+ trim_result.push_str(&text[last_end..text.len()]);
+
+ let parsed = SourceFile::parse(&trim_result, span::Edition::CURRENT);
+
+ if !parsed.errors().is_empty() {
+ return None;
+ }
+
+ let node: &SyntaxNode = &parsed.syntax_node();
+
+ if node.children().count() == 0 {
+ // C'mon, you should have at least one node other than SOURCE_FILE
+ return None;
+ }
+
+ Some(syntax_node_to_json(
+ node,
+ Some(InStringCtx {
+ offset: string_node.text_range_between_quotes()?.start().into(),
+ marker_positions,
+ }),
+ ))
+}
+
+struct InStringCtx {
+ offset: u32,
+ marker_positions: Vec<u32>,
+}
+
+#[cfg(test)]
+mod tests {
+ use expect_test::expect;
+
+ use crate::fixture;
+
+ fn check(ra_fixture: &str, expect: expect_test::Expect) {
+ let (analysis, file_id) = fixture::file(ra_fixture);
+ let syn = analysis.view_syntax_tree(file_id).unwrap();
+ expect.assert_eq(&syn)
+ }
+
+ #[test]
+ fn view_syntax_tree() {
+ // Basic syntax
+ check(
+ r#"fn foo() {}"#,
+ expect![[
+ r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":11,"children":[{"type":"Node","kind":"FN","start":0,"end":11,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":6,"children":[{"type":"Token","kind":"IDENT","start":3,"end":6}]},{"type":"Node","kind":"PARAM_LIST","start":6,"end":8,"children":[{"type":"Token","kind":"L_PAREN","start":6,"end":7},{"type":"Token","kind":"R_PAREN","start":7,"end":8}]},{"type":"Token","kind":"WHITESPACE","start":8,"end":9},{"type":"Node","kind":"BLOCK_EXPR","start":9,"end":11,"children":[{"type":"Node","kind":"STMT_LIST","start":9,"end":11,"children":[{"type":"Token","kind":"L_CURLY","start":9,"end":10},{"type":"Token","kind":"R_CURLY","start":10,"end":11}]}]}]}]}"#
+ ]],
+ );
+
+ check(
+ r#"
+fn test() {
+ assert!("
+ fn foo() {
+ }
+ ", "");
+}"#,
+ expect![[
+ r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":60,"children":[{"type":"Node","kind":"FN","start":0,"end":60,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":7,"children":[{"type":"Token","kind":"IDENT","start":3,"end":7}]},{"type":"Node","kind":"PARAM_LIST","start":7,"end":9,"children":[{"type":"Token","kind":"L_PAREN","start":7,"end":8},{"type":"Token","kind":"R_PAREN","start":8,"end":9}]},{"type":"Token","kind":"WHITESPACE","start":9,"end":10},{"type":"Node","kind":"BLOCK_EXPR","start":10,"end":60,"children":[{"type":"Node","kind":"STMT_LIST","start":10,"end":60,"children":[{"type":"Token","kind":"L_CURLY","start":10,"end":11},{"type":"Token","kind":"WHITESPACE","start":11,"end":16},{"type":"Node","kind":"EXPR_STMT","start":16,"end":58,"children":[{"type":"Node","kind":"MACRO_EXPR","start":16,"end":57,"children":[{"type":"Node","kind":"MACRO_CALL","start":16,"end":57,"children":[{"type":"Node","kind":"PATH","start":16,"end":22,"children":[{"type":"Node","kind":"PATH_SEGMENT","start":16,"end":22,"children":[{"type":"Node","kind":"NAME_REF","start":16,"end":22,"children":[{"type":"Token","kind":"IDENT","start":16,"end":22}]}]}]},{"type":"Token","kind":"BANG","start":22,"end":23},{"type":"Node","kind":"TOKEN_TREE","start":23,"end":57,"children":[{"type":"Token","kind":"L_PAREN","start":23,"end":24},{"type":"Node","kind":"STRING","start":24,"end":52,"children":[{"type":"Node","kind":"SOURCE_FILE","start":25,"end":51,"istart":0,"iend":26,"children":[{"type":"Token","kind":"WHITESPACE","start":25,"end":30,"istart":0,"iend":5},{"type":"Node","kind":"FN","start":30,"end":46,"istart":5,"iend":21,"children":[{"type":"Token","kind":"FN_KW","start":30,"end":32,"istart":5,"iend":7},{"type":"Token","kind":"WHITESPACE","start":32,"end":33,"istart":7,"iend":8},{"type":"Node","kind":"NAME","start":33,"end":36,"istart":8,"iend":11,"children":[{"type":"Token","kind":"IDENT","start":33,"end":36,"istart":8,"iend":11}]},{"type":"Node","kind":"PARAM_LIST","start":36,"end":38,"istart":11,"iend":13,"children":[{"type":"Token","kind":"L_PAREN","start":36,"end":37,"istart":11,"iend":12},{"type":"Token","kind":"R_PAREN","start":37,"end":38,"istart":12,"iend":13}]},{"type":"Token","kind":"WHITESPACE","start":38,"end":39,"istart":13,"iend":14},{"type":"Node","kind":"BLOCK_EXPR","start":39,"end":46,"istart":14,"iend":21,"children":[{"type":"Node","kind":"STMT_LIST","start":39,"end":46,"istart":14,"iend":21,"children":[{"type":"Token","kind":"L_CURLY","start":39,"end":40,"istart":14,"iend":15},{"type":"Token","kind":"WHITESPACE","start":40,"end":45,"istart":15,"iend":20},{"type":"Token","kind":"R_CURLY","start":45,"end":46,"istart":20,"iend":21}]}]}]},{"type":"Token","kind":"WHITESPACE","start":46,"end":51,"istart":21,"iend":26}]}]},{"type":"Token","kind":"COMMA","start":52,"end":53},{"type":"Token","kind":"WHITESPACE","start":53,"end":54},{"type":"Token","kind":"STRING","start":54,"end":56},{"type":"Token","kind":"R_PAREN","start":56,"end":57}]}]}]},{"type":"Token","kind":"SEMICOLON","start":57,"end":58}]},{"type":"Token","kind":"WHITESPACE","start":58,"end":59},{"type":"Token","kind":"R_CURLY","start":59,"end":60}]}]}]}]}"#
+ ]],
+ )
+ }
+
+ #[test]
+ fn view_syntax_tree_inside_string() {
+ check(
+ r#"fn test() {
+ assert!("
+$0fn foo() {
+}$0
+fn bar() {
+}
+ ", "");
+}"#,
+ expect![[
+ r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":65,"children":[{"type":"Node","kind":"FN","start":0,"end":65,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":7,"children":[{"type":"Token","kind":"IDENT","start":3,"end":7}]},{"type":"Node","kind":"PARAM_LIST","start":7,"end":9,"children":[{"type":"Token","kind":"L_PAREN","start":7,"end":8},{"type":"Token","kind":"R_PAREN","start":8,"end":9}]},{"type":"Token","kind":"WHITESPACE","start":9,"end":10},{"type":"Node","kind":"BLOCK_EXPR","start":10,"end":65,"children":[{"type":"Node","kind":"STMT_LIST","start":10,"end":65,"children":[{"type":"Token","kind":"L_CURLY","start":10,"end":11},{"type":"Token","kind":"WHITESPACE","start":11,"end":16},{"type":"Node","kind":"EXPR_STMT","start":16,"end":63,"children":[{"type":"Node","kind":"MACRO_EXPR","start":16,"end":62,"children":[{"type":"Node","kind":"MACRO_CALL","start":16,"end":62,"children":[{"type":"Node","kind":"PATH","start":16,"end":22,"children":[{"type":"Node","kind":"PATH_SEGMENT","start":16,"end":22,"children":[{"type":"Node","kind":"NAME_REF","start":16,"end":22,"children":[{"type":"Token","kind":"IDENT","start":16,"end":22}]}]}]},{"type":"Token","kind":"BANG","start":22,"end":23},{"type":"Node","kind":"TOKEN_TREE","start":23,"end":62,"children":[{"type":"Token","kind":"L_PAREN","start":23,"end":24},{"type":"Node","kind":"STRING","start":24,"end":57,"children":[{"type":"Node","kind":"SOURCE_FILE","start":25,"end":56,"istart":0,"iend":31,"children":[{"type":"Token","kind":"WHITESPACE","start":25,"end":26,"istart":0,"iend":1},{"type":"Node","kind":"FN","start":26,"end":38,"istart":1,"iend":13,"children":[{"type":"Token","kind":"FN_KW","start":26,"end":28,"istart":1,"iend":3},{"type":"Token","kind":"WHITESPACE","start":28,"end":29,"istart":3,"iend":4},{"type":"Node","kind":"NAME","start":29,"end":32,"istart":4,"iend":7,"children":[{"type":"Token","kind":"IDENT","start":29,"end":32,"istart":4,"iend":7}]},{"type":"Node","kind":"PARAM_LIST","start":32,"end":34,"istart":7,"iend":9,"children":[{"type":"Token","kind":"L_PAREN","start":32,"end":33,"istart":7,"iend":8},{"type":"Token","kind":"R_PAREN","start":33,"end":34,"istart":8,"iend":9}]},{"type":"Token","kind":"WHITESPACE","start":34,"end":35,"istart":9,"iend":10},{"type":"Node","kind":"BLOCK_EXPR","start":35,"end":38,"istart":10,"iend":13,"children":[{"type":"Node","kind":"STMT_LIST","start":35,"end":38,"istart":10,"iend":13,"children":[{"type":"Token","kind":"L_CURLY","start":35,"end":36,"istart":10,"iend":11},{"type":"Token","kind":"WHITESPACE","start":36,"end":37,"istart":11,"iend":12},{"type":"Token","kind":"R_CURLY","start":37,"end":38,"istart":12,"iend":13}]}]}]},{"type":"Token","kind":"WHITESPACE","start":38,"end":39,"istart":13,"iend":14},{"type":"Node","kind":"FN","start":39,"end":51,"istart":14,"iend":26,"children":[{"type":"Token","kind":"FN_KW","start":39,"end":41,"istart":14,"iend":16},{"type":"Token","kind":"WHITESPACE","start":41,"end":42,"istart":16,"iend":17},{"type":"Node","kind":"NAME","start":42,"end":45,"istart":17,"iend":20,"children":[{"type":"Token","kind":"IDENT","start":42,"end":45,"istart":17,"iend":20}]},{"type":"Node","kind":"PARAM_LIST","start":45,"end":47,"istart":20,"iend":22,"children":[{"type":"Token","kind":"L_PAREN","start":45,"end":46,"istart":20,"iend":21},{"type":"Token","kind":"R_PAREN","start":46,"end":47,"istart":21,"iend":22}]},{"type":"Token","kind":"WHITESPACE","start":47,"end":48,"istart":22,"iend":23},{"type":"Node","kind":"BLOCK_EXPR","start":48,"end":51,"istart":23,"iend":26,"children":[{"type":"Node","kind":"STMT_LIST","start":48,"end":51,"istart":23,"iend":26,"children":[{"type":"Token","kind":"L_CURLY","start":48,"end":49,"istart":23,"iend":24},{"type":"Token","kind":"WHITESPACE","start":49,"end":50,"istart":24,"iend":25},{"type":"Token","kind":"R_CURLY","start":50,"end":51,"istart":25,"iend":26}]}]}]},{"type":"Token","kind":"WHITESPACE","start":51,"end":56,"istart":26,"iend":31}]}]},{"type":"Token","kind":"COMMA","start":57,"end":58},{"type":"Token","kind":"WHITESPACE","start":58,"end":59},{"type":"Token","kind":"STRING","start":59,"end":61},{"type":"Token","kind":"R_PAREN","start":61,"end":62}]}]}]},{"type":"Token","kind":"SEMICOLON","start":62,"end":63}]},{"type":"Token","kind":"WHITESPACE","start":63,"end":64},{"type":"Token","kind":"R_CURLY","start":64,"end":65}]}]}]}]}"#
+ ]],
+ );
+
+ // With a raw string
+ check(
+ r###"fn test() {
+ assert!(r#"
+$0fn foo() {
+}$0
+fn bar() {
+}
+ "#, "");
+}"###,
+ expect![[
+ r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":68,"children":[{"type":"Node","kind":"FN","start":0,"end":68,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":7,"children":[{"type":"Token","kind":"IDENT","start":3,"end":7}]},{"type":"Node","kind":"PARAM_LIST","start":7,"end":9,"children":[{"type":"Token","kind":"L_PAREN","start":7,"end":8},{"type":"Token","kind":"R_PAREN","start":8,"end":9}]},{"type":"Token","kind":"WHITESPACE","start":9,"end":10},{"type":"Node","kind":"BLOCK_EXPR","start":10,"end":68,"children":[{"type":"Node","kind":"STMT_LIST","start":10,"end":68,"children":[{"type":"Token","kind":"L_CURLY","start":10,"end":11},{"type":"Token","kind":"WHITESPACE","start":11,"end":16},{"type":"Node","kind":"EXPR_STMT","start":16,"end":66,"children":[{"type":"Node","kind":"MACRO_EXPR","start":16,"end":65,"children":[{"type":"Node","kind":"MACRO_CALL","start":16,"end":65,"children":[{"type":"Node","kind":"PATH","start":16,"end":22,"children":[{"type":"Node","kind":"PATH_SEGMENT","start":16,"end":22,"children":[{"type":"Node","kind":"NAME_REF","start":16,"end":22,"children":[{"type":"Token","kind":"IDENT","start":16,"end":22}]}]}]},{"type":"Token","kind":"BANG","start":22,"end":23},{"type":"Node","kind":"TOKEN_TREE","start":23,"end":65,"children":[{"type":"Token","kind":"L_PAREN","start":23,"end":24},{"type":"Node","kind":"STRING","start":24,"end":60,"children":[{"type":"Node","kind":"SOURCE_FILE","start":27,"end":58,"istart":0,"iend":31,"children":[{"type":"Token","kind":"WHITESPACE","start":27,"end":28,"istart":0,"iend":1},{"type":"Node","kind":"FN","start":28,"end":40,"istart":1,"iend":13,"children":[{"type":"Token","kind":"FN_KW","start":28,"end":30,"istart":1,"iend":3},{"type":"Token","kind":"WHITESPACE","start":30,"end":31,"istart":3,"iend":4},{"type":"Node","kind":"NAME","start":31,"end":34,"istart":4,"iend":7,"children":[{"type":"Token","kind":"IDENT","start":31,"end":34,"istart":4,"iend":7}]},{"type":"Node","kind":"PARAM_LIST","start":34,"end":36,"istart":7,"iend":9,"children":[{"type":"Token","kind":"L_PAREN","start":34,"end":35,"istart":7,"iend":8},{"type":"Token","kind":"R_PAREN","start":35,"end":36,"istart":8,"iend":9}]},{"type":"Token","kind":"WHITESPACE","start":36,"end":37,"istart":9,"iend":10},{"type":"Node","kind":"BLOCK_EXPR","start":37,"end":40,"istart":10,"iend":13,"children":[{"type":"Node","kind":"STMT_LIST","start":37,"end":40,"istart":10,"iend":13,"children":[{"type":"Token","kind":"L_CURLY","start":37,"end":38,"istart":10,"iend":11},{"type":"Token","kind":"WHITESPACE","start":38,"end":39,"istart":11,"iend":12},{"type":"Token","kind":"R_CURLY","start":39,"end":40,"istart":12,"iend":13}]}]}]},{"type":"Token","kind":"WHITESPACE","start":40,"end":41,"istart":13,"iend":14},{"type":"Node","kind":"FN","start":41,"end":53,"istart":14,"iend":26,"children":[{"type":"Token","kind":"FN_KW","start":41,"end":43,"istart":14,"iend":16},{"type":"Token","kind":"WHITESPACE","start":43,"end":44,"istart":16,"iend":17},{"type":"Node","kind":"NAME","start":44,"end":47,"istart":17,"iend":20,"children":[{"type":"Token","kind":"IDENT","start":44,"end":47,"istart":17,"iend":20}]},{"type":"Node","kind":"PARAM_LIST","start":47,"end":49,"istart":20,"iend":22,"children":[{"type":"Token","kind":"L_PAREN","start":47,"end":48,"istart":20,"iend":21},{"type":"Token","kind":"R_PAREN","start":48,"end":49,"istart":21,"iend":22}]},{"type":"Token","kind":"WHITESPACE","start":49,"end":50,"istart":22,"iend":23},{"type":"Node","kind":"BLOCK_EXPR","start":50,"end":53,"istart":23,"iend":26,"children":[{"type":"Node","kind":"STMT_LIST","start":50,"end":53,"istart":23,"iend":26,"children":[{"type":"Token","kind":"L_CURLY","start":50,"end":51,"istart":23,"iend":24},{"type":"Token","kind":"WHITESPACE","start":51,"end":52,"istart":24,"iend":25},{"type":"Token","kind":"R_CURLY","start":52,"end":53,"istart":25,"iend":26}]}]}]},{"type":"Token","kind":"WHITESPACE","start":53,"end":58,"istart":26,"iend":31}]}]},{"type":"Token","kind":"COMMA","start":60,"end":61},{"type":"Token","kind":"WHITESPACE","start":61,"end":62},{"type":"Token","kind":"STRING","start":62,"end":64},{"type":"Token","kind":"R_PAREN","start":64,"end":65}]}]}]},{"type":"Token","kind":"SEMICOLON","start":65,"end":66}]},{"type":"Token","kind":"WHITESPACE","start":66,"end":67},{"type":"Token","kind":"R_CURLY","start":67,"end":68}]}]}]}]}"#
+ ]],
+ );
+
+ // With a raw string
+ check(
+ r###"fn test() {
+ assert!(r$0#"
+fn foo() {
+}
+fn bar() {
+}"$0#, "");
+}"###,
+ expect![[
+ r#"{"type":"Node","kind":"SOURCE_FILE","start":0,"end":63,"children":[{"type":"Node","kind":"FN","start":0,"end":63,"children":[{"type":"Token","kind":"FN_KW","start":0,"end":2},{"type":"Token","kind":"WHITESPACE","start":2,"end":3},{"type":"Node","kind":"NAME","start":3,"end":7,"children":[{"type":"Token","kind":"IDENT","start":3,"end":7}]},{"type":"Node","kind":"PARAM_LIST","start":7,"end":9,"children":[{"type":"Token","kind":"L_PAREN","start":7,"end":8},{"type":"Token","kind":"R_PAREN","start":8,"end":9}]},{"type":"Token","kind":"WHITESPACE","start":9,"end":10},{"type":"Node","kind":"BLOCK_EXPR","start":10,"end":63,"children":[{"type":"Node","kind":"STMT_LIST","start":10,"end":63,"children":[{"type":"Token","kind":"L_CURLY","start":10,"end":11},{"type":"Token","kind":"WHITESPACE","start":11,"end":16},{"type":"Node","kind":"EXPR_STMT","start":16,"end":61,"children":[{"type":"Node","kind":"MACRO_EXPR","start":16,"end":60,"children":[{"type":"Node","kind":"MACRO_CALL","start":16,"end":60,"children":[{"type":"Node","kind":"PATH","start":16,"end":22,"children":[{"type":"Node","kind":"PATH_SEGMENT","start":16,"end":22,"children":[{"type":"Node","kind":"NAME_REF","start":16,"end":22,"children":[{"type":"Token","kind":"IDENT","start":16,"end":22}]}]}]},{"type":"Token","kind":"BANG","start":22,"end":23},{"type":"Node","kind":"TOKEN_TREE","start":23,"end":60,"children":[{"type":"Token","kind":"L_PAREN","start":23,"end":24},{"type":"Node","kind":"STRING","start":24,"end":55,"children":[{"type":"Node","kind":"SOURCE_FILE","start":27,"end":53,"istart":0,"iend":26,"children":[{"type":"Token","kind":"WHITESPACE","start":27,"end":28,"istart":0,"iend":1},{"type":"Node","kind":"FN","start":28,"end":40,"istart":1,"iend":13,"children":[{"type":"Token","kind":"FN_KW","start":28,"end":30,"istart":1,"iend":3},{"type":"Token","kind":"WHITESPACE","start":30,"end":31,"istart":3,"iend":4},{"type":"Node","kind":"NAME","start":31,"end":34,"istart":4,"iend":7,"children":[{"type":"Token","kind":"IDENT","start":31,"end":34,"istart":4,"iend":7}]},{"type":"Node","kind":"PARAM_LIST","start":34,"end":36,"istart":7,"iend":9,"children":[{"type":"Token","kind":"L_PAREN","start":34,"end":35,"istart":7,"iend":8},{"type":"Token","kind":"R_PAREN","start":35,"end":36,"istart":8,"iend":9}]},{"type":"Token","kind":"WHITESPACE","start":36,"end":37,"istart":9,"iend":10},{"type":"Node","kind":"BLOCK_EXPR","start":37,"end":40,"istart":10,"iend":13,"children":[{"type":"Node","kind":"STMT_LIST","start":37,"end":40,"istart":10,"iend":13,"children":[{"type":"Token","kind":"L_CURLY","start":37,"end":38,"istart":10,"iend":11},{"type":"Token","kind":"WHITESPACE","start":38,"end":39,"istart":11,"iend":12},{"type":"Token","kind":"R_CURLY","start":39,"end":40,"istart":12,"iend":13}]}]}]},{"type":"Token","kind":"WHITESPACE","start":40,"end":41,"istart":13,"iend":14},{"type":"Node","kind":"FN","start":41,"end":53,"istart":14,"iend":26,"children":[{"type":"Token","kind":"FN_KW","start":41,"end":43,"istart":14,"iend":16},{"type":"Token","kind":"WHITESPACE","start":43,"end":44,"istart":16,"iend":17},{"type":"Node","kind":"NAME","start":44,"end":47,"istart":17,"iend":20,"children":[{"type":"Token","kind":"IDENT","start":44,"end":47,"istart":17,"iend":20}]},{"type":"Node","kind":"PARAM_LIST","start":47,"end":49,"istart":20,"iend":22,"children":[{"type":"Token","kind":"L_PAREN","start":47,"end":48,"istart":20,"iend":21},{"type":"Token","kind":"R_PAREN","start":48,"end":49,"istart":21,"iend":22}]},{"type":"Token","kind":"WHITESPACE","start":49,"end":50,"istart":22,"iend":23},{"type":"Node","kind":"BLOCK_EXPR","start":50,"end":53,"istart":23,"iend":26,"children":[{"type":"Node","kind":"STMT_LIST","start":50,"end":53,"istart":23,"iend":26,"children":[{"type":"Token","kind":"L_CURLY","start":50,"end":51,"istart":23,"iend":24},{"type":"Token","kind":"WHITESPACE","start":51,"end":52,"istart":24,"iend":25},{"type":"Token","kind":"R_CURLY","start":52,"end":53,"istart":25,"iend":26}]}]}]}]}]},{"type":"Token","kind":"COMMA","start":55,"end":56},{"type":"Token","kind":"WHITESPACE","start":56,"end":57},{"type":"Token","kind":"STRING","start":57,"end":59},{"type":"Token","kind":"R_PAREN","start":59,"end":60}]}]}]},{"type":"Token","kind":"SEMICOLON","start":60,"end":61}]},{"type":"Token","kind":"WHITESPACE","start":61,"end":62},{"type":"Token","kind":"R_CURLY","start":62,"end":63}]}]}]}]}"#
+ ]],
+ );
+ }
+}
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index 7ac70efe2d..d01dc5fba1 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -136,15 +136,13 @@ pub(crate) fn handle_memory_usage(state: &mut GlobalState, _: ()) -> anyhow::Res
Ok(out)
}
-pub(crate) fn handle_syntax_tree(
+pub(crate) fn handle_view_syntax_tree(
snap: GlobalStateSnapshot,
- params: lsp_ext::SyntaxTreeParams,
+ params: lsp_ext::ViewSyntaxTreeParams,
) -> anyhow::Result<String> {
- let _p = tracing::info_span!("handle_syntax_tree").entered();
+ let _p = tracing::info_span!("handle_view_syntax_tree").entered();
let id = from_proto::file_id(&snap, &params.text_document.uri)?;
- let line_index = snap.file_line_index(id)?;
- let text_range = params.range.and_then(|r| from_proto::text_range(&line_index, r).ok());
- let res = snap.analysis.syntax_tree(id, text_range)?;
+ let res = snap.analysis.view_syntax_tree(id)?;
Ok(res)
}
diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs
index f50cbba7ac..134de92fea 100644
--- a/crates/rust-analyzer/src/lsp/ext.rs
+++ b/crates/rust-analyzer/src/lsp/ext.rs
@@ -108,19 +108,18 @@ impl Request for RebuildProcMacros {
const METHOD: &'static str = "rust-analyzer/rebuildProcMacros";
}
-pub enum SyntaxTree {}
+pub enum ViewSyntaxTree {}
-impl Request for SyntaxTree {
- type Params = SyntaxTreeParams;
+impl Request for ViewSyntaxTree {
+ type Params = ViewSyntaxTreeParams;
type Result = String;
- const METHOD: &'static str = "rust-analyzer/syntaxTree";
+ const METHOD: &'static str = "rust-analyzer/viewSyntaxTree";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
-pub struct SyntaxTreeParams {
+pub struct ViewSyntaxTreeParams {
pub text_document: TextDocumentIdentifier,
- pub range: Option<Range>,
}
pub enum ViewHir {}
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 97657b9265..d6dc8b521f 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -1145,7 +1145,7 @@ impl GlobalState {
.on::<RETRY, lsp_ext::WorkspaceSymbol>(handlers::handle_workspace_symbol)
.on::<NO_RETRY, lsp_ext::Ssr>(handlers::handle_ssr)
.on::<NO_RETRY, lsp_ext::ViewRecursiveMemoryLayout>(handlers::handle_view_recursive_memory_layout)
- .on::<NO_RETRY, lsp_ext::SyntaxTree>(handlers::handle_syntax_tree)
+ .on::<NO_RETRY, lsp_ext::ViewSyntaxTree>(handlers::handle_view_syntax_tree)
.on::<NO_RETRY, lsp_ext::ViewHir>(handlers::handle_view_hir)
.on::<NO_RETRY, lsp_ext::ViewMir>(handlers::handle_view_mir)
.on::<NO_RETRY, lsp_ext::InterpretFunction>(handlers::handle_interpret_function)
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 21ac3a5a26..a632fc6f5f 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
<!---
-lsp/ext.rs hash: 6dd762ae19630ec0
+lsp/ext.rs hash: 2d8604825c458288
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
@@ -710,6 +710,23 @@ interface SyntaxTreeParams {
Returns textual representation of a parse tree for the file/selected region.
Primarily for debugging, but very useful for all people working on rust-analyzer itself.
+## View Syntax Tree
+
+**Method:** `rust-analyzer/viewSyntaxTree`
+
+**Request:**
+
+```typescript
+interface ViewSyntaxTreeParams {
+ textDocument: TextDocumentIdentifier,
+}
+```
+
+**Response:** `string`
+
+Returns json representation of the file's syntax tree.
+Used to create a treeView for debugging and working on rust-analyzer itself.
+
## View Hir
**Method:** `rust-analyzer/viewHir`
diff --git a/editors/code/package.json b/editors/code/package.json
index 62ef84e994..26cd49d9d2 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -109,11 +109,6 @@
],
"commands": [
{
- "command": "rust-analyzer.syntaxTree",
- "title": "Show Syntax Tree",
- "category": "rust-analyzer (debug command)"
- },
- {
"command": "rust-analyzer.viewHir",
"title": "View Hir",
"category": "rust-analyzer (debug command)"
@@ -289,6 +284,30 @@
"category": "rust-analyzer"
},
{
+ "command": "rust-analyzer.syntaxTreeReveal",
+ "title": "Reveal Syntax Element",
+ "icon": "$(search)",
+ "category": "rust-analyzer (syntax tree)"
+ },
+ {
+ "command": "rust-analyzer.syntaxTreeCopy",
+ "title": "Copy",
+ "icon": "$(copy)",
+ "category": "rust-analyzer (syntax tree)"
+ },
+ {
+ "command": "rust-analyzer.syntaxTreeHideWhitespace",
+ "title": "Hide Whitespace",
+ "icon": "$(filter)",
+ "category": "rust-analyzer (syntax tree)"
+ },
+ {
+ "command": "rust-analyzer.syntaxTreeShowWhitespace",
+ "title": "Show Whitespace",
+ "icon": "$(filter-filled)",
+ "category": "rust-analyzer (syntax tree)"
+ },
+ {
"command": "rust-analyzer.viewMemoryLayout",
"title": "View Memory Layout",
"category": "rust-analyzer"
@@ -345,6 +364,11 @@
"default": true,
"type": "boolean"
},
+ "rust-analyzer.showSyntaxTree": {
+ "markdownDescription": "Whether to show the syntax tree view.",
+ "default": true,
+ "type": "boolean"
+ },
"rust-analyzer.testExplorer": {
"markdownDescription": "Whether to show the test explorer.",
"default": false,
@@ -2944,17 +2968,6 @@
"pattern": "$rustc"
}
],
- "colors": [
- {
- "id": "rust_analyzer.syntaxTreeBorder",
- "description": "Color of the border displayed in the Rust source code for the selected syntax node (see \"Show Syntax Tree\" command)",
- "defaults": {
- "dark": "#ffffff",
- "light": "#b700ff",
- "highContrast": "#b700ff"
- }
- }
- ],
"semanticTokenTypes": [
{
"id": "angle",
@@ -3275,10 +3288,6 @@
"menus": {
"commandPalette": [
{
- "command": "rust-analyzer.syntaxTree",
- "when": "inRustProject"
- },
- {
"command": "rust-analyzer.viewHir",
"when": "inRustProject"
},
@@ -3360,6 +3369,22 @@
},
{
"command": "rust-analyzer.openWalkthrough"
+ },
+ {
+ "command": "rust-analyzer.syntaxTreeReveal",
+ "when": "false"
+ },
+ {
+ "command": "rust-analyzer.syntaxTreeCopy",
+ "when": "false"
+ },
+ {
+ "command": "rust-analyzer.syntaxTreeHideWhitespace",
+ "when": "false"
+ },
+ {
+ "command": "rust-analyzer.syntaxTreeShowWhitespace",
+ "when": "false"
}
],
"editor/context": [
@@ -3373,6 +3398,30 @@
"when": "inRustProject && editorTextFocus && editorLangId == rust",
"group": "navigation@1001"
}
+ ],
+ "view/title": [
+ {
+ "command": "rust-analyzer.syntaxTreeHideWhitespace",
+ "group": "navigation",
+ "when": "view == rustSyntaxTree && !rustSyntaxTree.hideWhitespace"
+ },
+ {
+ "command": "rust-analyzer.syntaxTreeShowWhitespace",
+ "group": "navigation",
+ "when": "view == rustSyntaxTree && rustSyntaxTree.hideWhitespace"
+ }
+ ],
+ "view/item/context": [
+ {
+ "command": "rust-analyzer.syntaxTreeCopy",
+ "group": "inline",
+ "when": "view == rustSyntaxTree"
+ },
+ {
+ "command": "rust-analyzer.syntaxTreeReveal",
+ "group": "inline",
+ "when": "view == rustSyntaxTree"
+ }
]
},
"views": {
@@ -3382,6 +3431,22 @@
"name": "Rust Dependencies",
"when": "inRustProject && config.rust-analyzer.showDependenciesExplorer"
}
+ ],
+ "rustSyntaxTreeContainer": [
+ {
+ "id": "rustSyntaxTree",
+ "name": "Rust Syntax Tree",
+ "when": "inRustProject && config.rust-analyzer.showSyntaxTree"
+ }
+ ]
+ },
+ "viewsContainers": {
+ "activitybar": [
+ {
+ "id": "rustSyntaxTreeContainer",
+ "title": "Rust Syntax Tree",
+ "icon": "$(list-tree)"
+ }
]
},
"jsonValidation": [
diff --git a/editors/code/src/ast_inspector.ts b/editors/code/src/ast_inspector.ts
deleted file mode 100644
index 35b705c477..0000000000
--- a/editors/code/src/ast_inspector.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-import * as vscode from "vscode";
-
-import type { Ctx, Disposable } from "./ctx";
-import { type RustEditor, isRustEditor, unwrapUndefinable } from "./util";
-
-// FIXME: consider implementing this via the Tree View API?
-// https://code.visualstudio.com/api/extension-guides/tree-view
-export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable {
- private readonly astDecorationType = vscode.window.createTextEditorDecorationType({
- borderColor: new vscode.ThemeColor("rust_analyzer.syntaxTreeBorder"),
- borderStyle: "solid",
- borderWidth: "2px",
- });
- private rustEditor: undefined | RustEditor;
-
- // Lazy rust token range -> syntax tree file range.
- private readonly rust2Ast = new Lazy(() => {
- const astEditor = this.findAstTextEditor();
- if (!this.rustEditor || !astEditor) return undefined;
-
- const buf: [vscode.Range, vscode.Range][] = [];
- for (let i = 0; i < astEditor.document.lineCount; ++i) {
- const astLine = astEditor.document.lineAt(i);
-
- // Heuristically look for nodes with quoted text (which are token nodes)
- const isTokenNode = astLine.text.lastIndexOf('"') >= 0;
- if (!isTokenNode) continue;
-
- const rustRange = this.parseRustTextRange(this.rustEditor.document, astLine.text);
- if (!rustRange) continue;
-
- buf.push([rustRange, this.findAstNodeRange(astLine)]);
- }
- return buf;
- });
-
- constructor(ctx: Ctx) {
- ctx.pushExtCleanup(
- vscode.languages.registerHoverProvider({ scheme: "rust-analyzer" }, this),
- );
- ctx.pushExtCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
- vscode.workspace.onDidCloseTextDocument(
- this.onDidCloseTextDocument,
- this,
- ctx.subscriptions,
- );
- vscode.workspace.onDidChangeTextDocument(
- this.onDidChangeTextDocument,
- this,
- ctx.subscriptions,
- );
- vscode.window.onDidChangeVisibleTextEditors(
- this.onDidChangeVisibleTextEditors,
- this,
- ctx.subscriptions,
- );
- }
- dispose() {
- this.setRustEditor(undefined);
- }
-
- private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
- if (
- this.rustEditor &&
- event.document.uri.toString() === this.rustEditor.document.uri.toString()
- ) {
- this.rust2Ast.reset();
- }
- }
-
- private onDidCloseTextDocument(doc: vscode.TextDocument) {
- if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
- this.setRustEditor(undefined);
- }
- }
-
- private onDidChangeVisibleTextEditors(editors: readonly vscode.TextEditor[]) {
- if (!this.findAstTextEditor()) {
- this.setRustEditor(undefined);
- return;
- }
- this.setRustEditor(editors.find(isRustEditor));
- }
-
- private findAstTextEditor(): undefined | vscode.TextEditor {
- return vscode.window.visibleTextEditors.find(
- (it) => it.document.uri.scheme === "rust-analyzer",
- );
- }
-
- private setRustEditor(newRustEditor: undefined | RustEditor) {
- if (this.rustEditor && this.rustEditor !== newRustEditor) {
- this.rustEditor.setDecorations(this.astDecorationType, []);
- this.rust2Ast.reset();
- }
- this.rustEditor = newRustEditor;
- }
-
- // additional positional params are omitted
- provideDefinition(
- doc: vscode.TextDocument,
- pos: vscode.Position,
- ): vscode.ProviderResult<vscode.DefinitionLink[]> {
- if (!this.rustEditor || doc.uri.toString() !== this.rustEditor.document.uri.toString()) {
- return;
- }
-
- const astEditor = this.findAstTextEditor();
- if (!astEditor) return;
-
- const rust2AstRanges = this.rust2Ast
- .get()
- ?.find(([rustRange, _]) => rustRange.contains(pos));
- if (!rust2AstRanges) return;
-
- const [rustFileRange, astFileRange] = rust2AstRanges;
-
- astEditor.revealRange(astFileRange);
- astEditor.selection = new vscode.Selection(astFileRange.start, astFileRange.end);
-
- return [
- {
- targetRange: astFileRange,
- targetUri: astEditor.document.uri,
- originSelectionRange: rustFileRange,
- targetSelectionRange: astFileRange,
- },
- ];
- }
-
- // additional positional params are omitted
- provideHover(
- doc: vscode.TextDocument,
- hoverPosition: vscode.Position,
- ): vscode.ProviderResult<vscode.Hover> {
- if (!this.rustEditor) return;
-
- const astFileLine = doc.lineAt(hoverPosition.line);
-
- const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text);
- if (!rustFileRange) return;
-
- this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]);
- this.rustEditor.revealRange(rustFileRange);
-
- const rustSourceCode = this.rustEditor.document.getText(rustFileRange);
- const astFileRange = this.findAstNodeRange(astFileLine);
-
- return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange);
- }
-
- private findAstNodeRange(astLine: vscode.TextLine): vscode.Range {
- const lineOffset = astLine.range.start;
- const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
- const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
- return new vscode.Range(begin, end);
- }
-
- private parseRustTextRange(
- doc: vscode.TextDocument,
- astLine: string,
- ): undefined | vscode.Range {
- const parsedRange = /(\d+)\.\.(\d+)/.exec(astLine);
- if (!parsedRange) return;
-
- const [begin, end] = parsedRange.slice(1).map((off) => this.positionAt(doc, +off));
- const actualBegin = unwrapUndefinable(begin);
- const actualEnd = unwrapUndefinable(end);
- return new vscode.Range(actualBegin, actualEnd);
- }
-
- // Memoize the last value, otherwise the CPU is at 100% single core
- // with quadratic lookups when we build rust2Ast cache
- cache?: { doc: vscode.TextDocument; offset: number; line: number };
-
- positionAt(doc: vscode.TextDocument, targetOffset: number): vscode.Position {
- if (doc.eol === vscode.EndOfLine.LF) {
- return doc.positionAt(targetOffset);
- }
-
- // Dirty workaround for crlf line endings
- // We are still in this prehistoric era of carriage returns here...
-
- let line = 0;
- let offset = 0;
-
- const cache = this.cache;
- if (cache?.doc === doc && cache.offset <= targetOffset) {
- ({ line, offset } = cache);
- }
-
- while (true) {
- const lineLenWithLf = doc.lineAt(line).text.length + 1;
- if (offset + lineLenWithLf > targetOffset) {
- this.cache = { doc, offset, line };
- return doc.positionAt(targetOffset + line);
- }
- offset += lineLenWithLf;
- line += 1;
- }
- }
-}
-
-class Lazy<T> {
- val: undefined | T;
-
- constructor(private readonly compute: () => undefined | T) {}
-
- get() {
- return this.val ?? (this.val = this.compute());
- }
-
- reset() {
- this.val = undefined;
- }
-}
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 73e39c900e..b3aa04af7e 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -15,7 +15,6 @@ import {
createTaskFromRunnable,
createCargoArgs,
} from "./run";
-import { AstInspector } from "./ast_inspector";
import {
isRustDocument,
isCargoRunnableArgs,
@@ -31,8 +30,8 @@ import type { LanguageClient } from "vscode-languageclient/node";
import { HOVER_REFERENCE_COMMAND } from "./client";
import type { DependencyId } from "./dependencies_provider";
import { log } from "./util";
+import type { SyntaxElement } from "./syntax_tree_provider";
-export * from "./ast_inspector";
export * from "./run";
export function analyzerStatus(ctx: CtxInit): Cmd {
@@ -288,13 +287,13 @@ export function openCargoToml(ctx: CtxInit): Cmd {
export function revealDependency(ctx: CtxInit): Cmd {
return async (editor: RustEditor) => {
- if (!ctx.dependencies?.isInitialized()) {
+ if (!ctx.dependenciesProvider?.isInitialized()) {
return;
}
const documentPath = editor.document.uri.fsPath;
- const dep = ctx.dependencies?.getDependency(documentPath);
+ const dep = ctx.dependenciesProvider?.getDependency(documentPath);
if (dep) {
- await ctx.treeView?.reveal(dep, { select: true, expand: true });
+ await ctx.dependencyTreeView?.reveal(dep, { select: true, expand: true });
} else {
await revealParentChain(editor.document, ctx);
}
@@ -340,10 +339,10 @@ async function revealParentChain(document: RustDocument, ctx: CtxInit) {
// a open file referencing the old version
return;
}
- } while (!ctx.dependencies?.contains(documentPath));
+ } while (!ctx.dependenciesProvider?.contains(documentPath));
parentChain.reverse();
for (const idx in parentChain) {
- const treeView = ctx.treeView;
+ const treeView = ctx.dependencyTreeView;
if (!treeView) {
continue;
}
@@ -357,6 +356,77 @@ export async function execRevealDependency(e: RustEditor): Promise<void> {
await vscode.commands.executeCommand("rust-analyzer.revealDependency", e);
}
+export function syntaxTreeReveal(): Cmd {
+ return async (element: SyntaxElement) => {
+ const activeEditor = vscode.window.activeTextEditor;
+
+ if (activeEditor !== undefined) {
+ const start = activeEditor.document.positionAt(element.start);
+ const end = activeEditor.document.positionAt(element.end);
+
+ const newSelection = new vscode.Selection(start, end);
+
+ activeEditor.selection = newSelection;
+ activeEditor.revealRange(newSelection);
+ }
+ };
+}
+
+function elementToString(
+ activeDocument: vscode.TextDocument,
+ element: SyntaxElement,
+ depth: number = 0,
+): string {
+ let result = " ".repeat(depth);
+ const start = element.istart ?? element.start;
+ const end = element.iend ?? element.end;
+
+ result += `${element.kind}@${start}..${end}`;
+
+ if (element.type === "Token") {
+ const startPosition = activeDocument.positionAt(element.start);
+ const endPosition = activeDocument.positionAt(element.end);
+ const text = activeDocument.getText(new vscode.Range(startPosition, endPosition));
+ // JSON.stringify quotes and escapes the string for us.
+ result += ` ${JSON.stringify(text)}\n`;
+ } else {
+ result += "\n";
+ for (const child of element.children) {
+ result += elementToString(activeDocument, child, depth + 1);
+ }
+ }
+
+ return result;
+}
+
+export function syntaxTreeCopy(): Cmd {
+ return async (element: SyntaxElement) => {
+ const activeDocument = vscode.window.activeTextEditor?.document;
+ if (!activeDocument) {
+ return;
+ }
+
+ const result = elementToString(activeDocument, element);
+ await vscode.env.clipboard.writeText(result);
+ };
+}
+
+export function syntaxTreeHideWhitespace(ctx: CtxInit): Cmd {
+ return async () => {
+ if (ctx.syntaxTreeProvider !== undefined) {
+ await ctx.syntaxTreeProvider.toggleWhitespace();
+ }
+ };
+}
+
+export function syntaxTreeShowWhitespace(ctx: CtxInit): Cmd {
+ return async () => {
+ if (ctx.syntaxTreeProvider !== undefined) {
+ await ctx.syntaxTreeProvider.toggleWhitespace();
+ }
+ };
+}
+
export function ssr(ctx: CtxInit): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;
@@ -426,89 +496,6 @@ export function serverVersion(ctx: CtxInit): Cmd {
};
}
-// Opens the virtual file that will show the syntax tree
-//
-// The contents of the file come from the `TextDocumentContentProvider`
-export function syntaxTree(ctx: CtxInit): Cmd {
- const tdcp = new (class implements vscode.TextDocumentContentProvider {
- readonly uri = vscode.Uri.parse("rust-analyzer-syntax-tree://syntaxtree/tree.rast");
- readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
- constructor() {
- vscode.workspace.onDidChangeTextDocument(
- this.onDidChangeTextDocument,
- this,
- ctx.subscriptions,
- );
- vscode.window.onDidChangeActiveTextEditor(
- this.onDidChangeActiveTextEditor,
- this,
- ctx.subscriptions,
- );
- }
-
- private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
- if (isRustDocument(event.document)) {
- // We need to order this after language server updates, but there's no API for that.
- // Hence, good old sleep().
- void sleep(10).then(() => this.eventEmitter.fire(this.uri));
- }
- }
- private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
- if (editor && isRustEditor(editor)) {
- this.eventEmitter.fire(this.uri);
- }
- }
-
- async provideTextDocumentContent(
- uri: vscode.Uri,
- ct: vscode.CancellationToken,
- ): Promise<string> {
- const rustEditor = ctx.activeRustEditor;
- if (!rustEditor) return "";
- const client = ctx.client;
-
- // When the range based query is enabled we take the range of the selection
- const range =
- uri.query === "range=true" && !rustEditor.selection.isEmpty
- ? client.code2ProtocolConverter.asRange(rustEditor.selection)
- : null;
-
- const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range };
- return client.sendRequest(ra.syntaxTree, params, ct);
- }
-
- get onDidChange(): vscode.Event<vscode.Uri> {
- return this.eventEmitter.event;
- }
- })();
-
- ctx.pushExtCleanup(new AstInspector(ctx));
- ctx.pushExtCleanup(
- vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-syntax-tree", tdcp),
- );
- ctx.pushExtCleanup(
- vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
- brackets: [["[", ")"]],
- }),
- );
-
- return async () => {
- const editor = vscode.window.activeTextEditor;
- const rangeEnabled = !!editor && !editor.selection.isEmpty;
-
- const uri = rangeEnabled ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`) : tdcp.uri;
-
- const document = await vscode.workspace.openTextDocument(uri);
-
- tdcp.eventEmitter.fire(uri);
-
- void (await vscode.window.showTextDocument(document, {
- viewColumn: vscode.ViewColumn.Two,
- preserveFocus: true,
- }));
- };
-}
-
function viewHirOrMir(ctx: CtxInit, xir: "hir" | "mir"): Cmd {
const viewXir = xir === "hir" ? "viewHir" : "viewMir";
const requestType = xir === "hir" ? ra.viewHir : ra.viewMir;
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 720c473c5b..d1467a4e82 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -351,6 +351,10 @@ export class Config {
return this.get<boolean>("showDependenciesExplorer");
}
+ get showSyntaxTree() {
+ return this.get<boolean>("showSyntaxTree");
+ }
+
get statusBarClickAction() {
return this.get<string>("statusBar.clickAction");
}
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index 37a54abf71..5550bfa655 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -19,6 +19,7 @@ import {
RustDependenciesProvider,
type DependencyId,
} from "./dependencies_provider";
+import { SyntaxTreeProvider, type SyntaxElement } from "./syntax_tree_provider";
import { execRevealDependency } from "./commands";
import { PersistentState } from "./persistent_state";
import { bootstrap } from "./bootstrap";
@@ -84,8 +85,12 @@ export class Ctx implements RustAnalyzerExtensionApi {
private commandFactories: Record<string, CommandFactory>;
private commandDisposables: Disposable[];
private unlinkedFiles: vscode.Uri[];
- private _dependencies: RustDependenciesProvider | undefined;
- private _treeView: vscode.TreeView<Dependency | DependencyFile | DependencyId> | undefined;
+ private _dependenciesProvider: RustDependenciesProvider | undefined;
+ private _dependencyTreeView:
+ | vscode.TreeView<Dependency | DependencyFile | DependencyId>
+ | undefined;
+ private _syntaxTreeProvider: SyntaxTreeProvider | undefined;
+ private _syntaxTreeView: vscode.TreeView<SyntaxElement> | undefined;
private lastStatus: ServerStatusParams | { health: "stopped" } = { health: "stopped" };
private _serverVersion: string;
private statusBarActiveEditorListener: Disposable;
@@ -102,12 +107,20 @@ export class Ctx implements RustAnalyzerExtensionApi {
return this._client;
}
- get treeView() {
- return this._treeView;
+ get dependencyTreeView() {
+ return this._dependencyTreeView;
}
- get dependencies() {
- return this._dependencies;
+ get dependenciesProvider() {
+ return this._dependenciesProvider;
+ }
+
+ get syntaxTreeView() {
+ return this._syntaxTreeView;
+ }
+
+ get syntaxTreeProvider() {
+ return this._syntaxTreeProvider;
}
constructor(
@@ -278,6 +291,9 @@ export class Ctx implements RustAnalyzerExtensionApi {
if (this.config.showDependenciesExplorer) {
this.prepareTreeDependenciesView(client);
}
+ if (this.config.showSyntaxTree) {
+ this.prepareSyntaxTreeView(client);
+ }
}
private prepareTreeDependenciesView(client: lc.LanguageClient) {
@@ -285,13 +301,13 @@ export class Ctx implements RustAnalyzerExtensionApi {
...this,
client: client,
};
- this._dependencies = new RustDependenciesProvider(ctxInit);
- this._treeView = vscode.window.createTreeView("rustDependencies", {
- treeDataProvider: this._dependencies,
+ this._dependenciesProvider = new RustDependenciesProvider(ctxInit);
+ this._dependencyTreeView = vscode.window.createTreeView("rustDependencies", {
+ treeDataProvider: this._dependenciesProvider,
showCollapseAll: true,
});
- this.pushExtCleanup(this._treeView);
+ this.pushExtCleanup(this._dependencyTreeView);
vscode.window.onDidChangeActiveTextEditor(async (e) => {
// we should skip documents that belong to the current workspace
if (this.shouldRevealDependency(e)) {
@@ -303,7 +319,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
}
});
- this.treeView?.onDidChangeVisibility(async (e) => {
+ this.dependencyTreeView?.onDidChangeVisibility(async (e) => {
if (e.visible) {
const activeEditor = vscode.window.activeTextEditor;
if (this.shouldRevealDependency(activeEditor)) {
@@ -322,10 +338,60 @@ export class Ctx implements RustAnalyzerExtensionApi {
e !== undefined &&
isRustEditor(e) &&
!isDocumentInWorkspace(e.document) &&
- (this.treeView?.visible || false)
+ (this.dependencyTreeView?.visible || false)
);
}
+ private prepareSyntaxTreeView(client: lc.LanguageClient) {
+ const ctxInit: CtxInit = {
+ ...this,
+ client: client,
+ };
+ this._syntaxTreeProvider = new SyntaxTreeProvider(ctxInit);
+ this._syntaxTreeView = vscode.window.createTreeView("rustSyntaxTree", {
+ treeDataProvider: this._syntaxTreeProvider,
+ showCollapseAll: true,
+ });
+
+ this.pushExtCleanup(this._syntaxTreeView);
+
+ vscode.window.onDidChangeActiveTextEditor(async () => {
+ if (this.syntaxTreeView?.visible) {
+ await this.syntaxTreeProvider?.refresh();
+ }
+ });
+
+ vscode.workspace.onDidChangeTextDocument(async () => {
+ if (this.syntaxTreeView?.visible) {
+ await this.syntaxTreeProvider?.refresh();
+ }
+ });
+
+ vscode.window.onDidChangeTextEditorSelection(async (e) => {
+ if (!this.syntaxTreeView?.visible || !isRustEditor(e.textEditor)) {
+ return;
+ }
+
+ const selection = e.selections[0];
+ if (selection === undefined) {
+ return;
+ }
+
+ const start = e.textEditor.document.offsetAt(selection.start);
+ const end = e.textEditor.document.offsetAt(selection.end);
+ const result = this.syntaxTreeProvider?.getElementByRange(start, end);
+ if (result !== undefined) {
+ await this.syntaxTreeView?.reveal(result);
+ }
+ });
+
+ this._syntaxTreeView.onDidChangeVisibility(async (e) => {
+ if (e.visible) {
+ await this.syntaxTreeProvider?.refresh();
+ }
+ });
+ }
+
async restart() {
// FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
await this.stopAndDispose();
@@ -423,7 +489,8 @@ export class Ctx implements RustAnalyzerExtensionApi {
} else {
statusBar.command = "rust-analyzer.openLogs";
}
- this.dependencies?.refresh();
+ this.dependenciesProvider?.refresh();
+ void this.syntaxTreeProvider?.refresh();
break;
case "warning":
statusBar.color = new vscode.ThemeColor("statusBarItem.warningForeground");
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index d52e314e21..af86d9efd1 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -48,6 +48,9 @@ export const runFlycheck = new lc.NotificationType<{
export const syntaxTree = new lc.RequestType<SyntaxTreeParams, string, void>(
"rust-analyzer/syntaxTree",
);
+export const viewSyntaxTree = new lc.RequestType<ViewSyntaxTreeParams, string, void>(
+ "rust-analyzer/viewSyntaxTree",
+);
export const viewCrateGraph = new lc.RequestType<ViewCrateGraphParams, string, void>(
"rust-analyzer/viewCrateGraph",
);
@@ -157,6 +160,7 @@ export type SyntaxTreeParams = {
textDocument: lc.TextDocumentIdentifier;
range: lc.Range | null;
};
+export type ViewSyntaxTreeParams = { textDocument: lc.TextDocumentIdentifier };
export type ViewCrateGraphParams = { full: boolean };
export type ViewItemTreeParams = { textDocument: lc.TextDocumentIdentifier };
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index fdf43f66f9..c84b69b66c 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -158,7 +158,6 @@ function createCommands(): Record<string, CommandFactory> {
matchingBrace: { enabled: commands.matchingBrace },
joinLines: { enabled: commands.joinLines },
parentModule: { enabled: commands.parentModule },
- syntaxTree: { enabled: commands.syntaxTree },
viewHir: { enabled: commands.viewHir },
viewMir: { enabled: commands.viewMir },
interpretFunction: { enabled: commands.interpretFunction },
@@ -199,6 +198,10 @@ function createCommands(): Record<string, CommandFactory> {
rename: { enabled: commands.rename },
openLogs: { enabled: commands.openLogs },
revealDependency: { enabled: commands.revealDependency },
+ syntaxTreeReveal: { enabled: commands.syntaxTreeReveal },
+ syntaxTreeCopy: { enabled: commands.syntaxTreeCopy },
+ syntaxTreeHideWhitespace: { enabled: commands.syntaxTreeHideWhitespace },
+ syntaxTreeShowWhitespace: { enabled: commands.syntaxTreeShowWhitespace },
};
}
diff --git a/editors/code/src/syntax_tree_provider.ts b/editors/code/src/syntax_tree_provider.ts
new file mode 100644
index 0000000000..c7e8007e83
--- /dev/null
+++ b/editors/code/src/syntax_tree_provider.ts
@@ -0,0 +1,301 @@
+import * as vscode from "vscode";
+
+import { isRustEditor, setContextValue } from "./util";
+import type { CtxInit } from "./ctx";
+import * as ra from "./lsp_ext";
+
+export class SyntaxTreeProvider implements vscode.TreeDataProvider<SyntaxElement> {
+ private _onDidChangeTreeData: vscode.EventEmitter<SyntaxElement | undefined | void> =
+ new vscode.EventEmitter<SyntaxElement | undefined | void>();
+ readonly onDidChangeTreeData: vscode.Event<SyntaxElement | undefined | void> =
+ this._onDidChangeTreeData.event;
+ ctx: CtxInit;
+ root: SyntaxNode | undefined;
+ hideWhitespace: boolean = false;
+
+ constructor(ctx: CtxInit) {
+ this.ctx = ctx;
+ }
+
+ getTreeItem(element: SyntaxElement): vscode.TreeItem {
+ return new SyntaxTreeItem(element);
+ }
+
+ getChildren(element?: SyntaxElement): vscode.ProviderResult<SyntaxElement[]> {
+ return this.getRawChildren(element);
+ }
+
+ getParent(element: SyntaxElement): vscode.ProviderResult<SyntaxElement> {
+ return element.parent;
+ }
+
+ resolveTreeItem(
+ item: SyntaxTreeItem,
+ element: SyntaxElement,
+ _token: vscode.CancellationToken,
+ ): vscode.ProviderResult<SyntaxTreeItem> {
+ const editor = vscode.window.activeTextEditor;
+
+ if (editor !== undefined) {
+ const start = editor.document.positionAt(element.start);
+ const end = editor.document.positionAt(element.end);
+ const range = new vscode.Range(start, end);
+
+ const text = editor.document.getText(range);
+ item.tooltip = new vscode.MarkdownString().appendCodeblock(text, "rust");
+ }
+
+ return item;
+ }
+
+ private getRawChildren(element?: SyntaxElement): SyntaxElement[] {
+ if (element?.type === "Node") {
+ if (this.hideWhitespace) {
+ return element.children.filter((e) => e.kind !== "WHITESPACE");
+ }
+
+ return element.children;
+ }
+
+ if (element?.type === "Token") {
+ return [];
+ }
+
+ if (element === undefined && this.root !== undefined) {
+ return [this.root];
+ }
+
+ return [];
+ }
+
+ async refresh(): Promise<void> {
+ const editor = vscode.window.activeTextEditor;
+
+ if (editor && isRustEditor(editor)) {
+ const params = { textDocument: { uri: editor.document.uri.toString() }, range: null };
+ const fileText = await this.ctx.client.sendRequest(ra.viewSyntaxTree, params);
+ this.root = JSON.parse(fileText, (_key, value: SyntaxElement) => {
+ if (value.type === "Node") {
+ for (const child of value.children) {
+ child.parent = value;
+ }
+ }
+
+ return value;
+ });
+ } else {
+ this.root = undefined;
+ }
+
+ this._onDidChangeTreeData.fire();
+ }
+
+ getElementByRange(start: number, end: number): SyntaxElement | undefined {
+ if (this.root === undefined) {
+ return undefined;
+ }
+
+ let result: SyntaxElement = this.root;
+
+ if (this.root.start === start && this.root.end === end) {
+ return result;
+ }
+
+ let children = this.getRawChildren(this.root);
+
+ outer: while (true) {
+ for (const child of children) {
+ if (child.start <= start && child.end >= end) {
+ result = child;
+ if (start === end && start === child.end) {
+ // When the cursor is on the very end of a token,
+ // we assume the user wants the next token instead.
+ continue;
+ }
+
+ if (child.type === "Token") {
+ return result;
+ } else {
+ children = this.getRawChildren(child);
+ continue outer;
+ }
+ }
+ }
+
+ return result;
+ }
+ }
+
+ async toggleWhitespace() {
+ this.hideWhitespace = !this.hideWhitespace;
+ this._onDidChangeTreeData.fire();
+ await setContextValue("rustSyntaxTree.hideWhitespace", this.hideWhitespace);
+ }
+}
+
+export type SyntaxNode = {
+ type: "Node";
+ kind: string;
+ start: number;
+ end: number;
+ istart?: number;
+ iend?: number;
+ children: SyntaxElement[];
+ parent?: SyntaxElement;
+};
+
+type SyntaxToken = {
+ type: "Token";
+ kind: string;
+ start: number;
+ end: number;
+ istart?: number;
+ iend?: number;
+ parent?: SyntaxElement;
+};
+
+export type SyntaxElement = SyntaxNode | SyntaxToken;
+
+export class SyntaxTreeItem extends vscode.TreeItem {
+ constructor(private readonly element: SyntaxElement) {
+ super(element.kind);
+ const icon = getIcon(element.kind);
+ if (element.type === "Node") {
+ this.contextValue = "syntaxNode";
+ this.iconPath = icon ?? new vscode.ThemeIcon("list-tree");
+ this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded;
+ } else {
+ this.contextValue = "syntaxToken";
+ this.iconPath = icon ?? new vscode.ThemeIcon("symbol-string");
+ this.collapsibleState = vscode.TreeItemCollapsibleState.None;
+ }
+
+ if (element.istart !== undefined && element.iend !== undefined) {
+ this.description = `${this.element.istart}..${this.element.iend}`;
+ } else {
+ this.description = `${this.element.start}..${this.element.end}`;
+ }
+ }
+}
+
+function getIcon(kind: string): vscode.ThemeIcon | undefined {
+ const icon = iconTable[kind];
+
+ if (icon !== undefined) {
+ return icon;
+ }
+
+ if (kind.endsWith("_KW")) {
+ return new vscode.ThemeIcon(
+ "symbol-keyword",
+ new vscode.ThemeColor("symbolIcon.keywordForeground"),
+ );
+ }
+
+ if (operators.includes(kind)) {
+ return new vscode.ThemeIcon(
+ "symbol-operator",
+ new vscode.ThemeColor("symbolIcon.operatorForeground"),
+ );
+ }
+
+ return undefined;
+}
+
+const iconTable: Record<string, vscode.ThemeIcon> = {
+ CALL_EXPR: new vscode.ThemeIcon("call-outgoing"),
+ COMMENT: new vscode.ThemeIcon("comment"),
+ ENUM: new vscode.ThemeIcon("symbol-enum", new vscode.ThemeColor("symbolIcon.enumForeground")),
+ FN: new vscode.ThemeIcon(
+ "symbol-function",
+ new vscode.ThemeColor("symbolIcon.functionForeground"),
+ ),
+ FLOAT_NUMBER: new vscode.ThemeIcon(
+ "symbol-number",
+ new vscode.ThemeColor("symbolIcon.numberForeground"),
+ ),
+ INDEX_EXPR: new vscode.ThemeIcon(
+ "symbol-array",
+ new vscode.ThemeColor("symbolIcon.arrayForeground"),
+ ),
+ INT_NUMBER: new vscode.ThemeIcon(
+ "symbol-number",
+ new vscode.ThemeColor("symbolIcon.numberForeground"),
+ ),
+ LITERAL: new vscode.ThemeIcon(
+ "symbol-misc",
+ new vscode.ThemeColor("symbolIcon.miscForeground"),
+ ),
+ MODULE: new vscode.ThemeIcon(
+ "symbol-module",
+ new vscode.ThemeColor("symbolIcon.moduleForeground"),
+ ),
+ METHOD_CALL_EXPR: new vscode.ThemeIcon("call-outgoing"),
+ PARAM: new vscode.ThemeIcon(
+ "symbol-parameter",
+ new vscode.ThemeColor("symbolIcon.parameterForeground"),
+ ),
+ RECORD_FIELD: new vscode.ThemeIcon(
+ "symbol-field",
+ new vscode.ThemeColor("symbolIcon.fieldForeground"),
+ ),
+ SOURCE_FILE: new vscode.ThemeIcon("file-code"),
+ STRING: new vscode.ThemeIcon("quote"),
+ STRUCT: new vscode.ThemeIcon(
+ "symbol-struct",
+ new vscode.ThemeColor("symbolIcon.structForeground"),
+ ),
+ TRAIT: new vscode.ThemeIcon(
+ "symbol-interface",
+ new vscode.ThemeColor("symbolIcon.interfaceForeground"),
+ ),
+ TYPE_PARAM: new vscode.ThemeIcon(
+ "symbol-type-parameter",
+ new vscode.ThemeColor("symbolIcon.typeParameterForeground"),
+ ),
+ VARIANT: new vscode.ThemeIcon(
+ "symbol-enum-member",
+ new vscode.ThemeColor("symbolIcon.enumMemberForeground"),
+ ),
+ WHITESPACE: new vscode.ThemeIcon("whitespace"),
+};
+
+const operators = [
+ "PLUS",
+ "PLUSEQ",
+ "MINUS",
+ "MINUSEQ",
+ "STAR",
+ "STAREQ",
+ "SLASH",
+ "SLASHEQ",
+ "PERCENT",
+ "PERCENTEQ",
+ "CARET",
+ "CARETEQ",
+ "AMP",
+ "AMPEQ",
+ "AMP2",
+ "PIPE",
+ "PIPEEQ",
+ "PIPE2",
+ "SHL",
+ "SHLEQ",
+ "SHR",
+ "SHREQ",
+ "EQ",
+ "EQ2",
+ "BANG",
+ "NEQ",
+ "L_ANGLE",
+ "LTEQ",
+ "R_ANGLE",
+ "GTEQ",
+ "COLON2",
+ "THIN_ARROW",
+ "FAT_ARROW",
+ "DOT",
+ "DOT2",
+ "DOT2EQ",
+ "AT",
+];