about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGiga Bowser <45986823+Giga-Bowser@users.noreply.github.com>2024-12-20 19:12:04 -0500
committerGiga Bowser <45986823+Giga-Bowser@users.noreply.github.com>2025-01-09 13:20:06 -0600
commit78c377f956ffb7c9d1cd5d1786f8873959681f50 (patch)
tree849b3e180a629bafd27abbe8c7695bf738142b9b
parentd8eb839305a43521b155cf741210a5c11a188924 (diff)
downloadrust-78c377f956ffb7c9d1cd5d1786f8873959681f50.tar.gz
rust-78c377f956ffb7c9d1cd5d1786f8873959681f50.zip
Add a new and improved syntax tree viewer
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/lib.rs5
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/syntax_tree.rs4
-rw-r--r--src/tools/rust-analyzer/crates/ide/src/view_syntax_tree.rs226
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs10
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/lsp/ext.rs14
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs1
-rw-r--r--src/tools/rust-analyzer/docs/dev/lsp-extensions.md19
-rw-r--r--src/tools/rust-analyzer/editors/code/package.json70
-rw-r--r--src/tools/rust-analyzer/editors/code/src/commands.ts33
-rw-r--r--src/tools/rust-analyzer/editors/code/src/config.ts4
-rw-r--r--src/tools/rust-analyzer/editors/code/src/ctx.ts69
-rw-r--r--src/tools/rust-analyzer/editors/code/src/lsp_ext.ts4
-rw-r--r--src/tools/rust-analyzer/editors/code/src/main.ts3
-rw-r--r--src/tools/rust-analyzer/editors/code/src/syntax_tree_provider.ts301
14 files changed, 759 insertions, 4 deletions
diff --git a/src/tools/rust-analyzer/crates/ide/src/lib.rs b/src/tools/rust-analyzer/crates/ide/src/lib.rs
index 6e7c718953c..5cb77e62016 100644
--- a/src/tools/rust-analyzer/crates/ide/src/lib.rs
+++ b/src/tools/rust-analyzer/crates/ide/src/lib.rs
@@ -56,6 +56,7 @@ mod view_hir;
 mod view_item_tree;
 mod view_memory_layout;
 mod view_mir;
+mod view_syntax_tree;
 
 use std::{iter, panic::UnwindSafe};
 
@@ -339,6 +340,10 @@ impl Analysis {
         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> {
         self.with_db(|db| view_hir::view_hir(db, position))
     }
diff --git a/src/tools/rust-analyzer/crates/ide/src/syntax_tree.rs b/src/tools/rust-analyzer/crates/ide/src/syntax_tree.rs
index e241cb82bd5..86c6676f928 100644
--- a/src/tools/rust-analyzer/crates/ide/src/syntax_tree.rs
+++ b/src/tools/rust-analyzer/crates/ide/src/syntax_tree.rs
@@ -4,9 +4,9 @@ use syntax::{
     AstNode, NodeOrToken, SourceFile, SyntaxKind::STRING, SyntaxToken, TextRange, TextSize,
 };
 
-// Feature: Show Syntax Tree
+// Feature: Show Debug Syntax Tree
 //
-// Shows the parse tree of the current file. It exists mostly for debugging
+// Shows the textual parse tree of the current file. It exists mostly for debugging
 // rust-analyzer itself.
 //
 // |===
diff --git a/src/tools/rust-analyzer/crates/ide/src/view_syntax_tree.rs b/src/tools/rust-analyzer/crates/ide/src/view_syntax_tree.rs
new file mode 100644
index 00000000000..b2adff0f36a
--- /dev/null
+++ b/src/tools/rust-analyzer/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/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs
index 7ac70efe2d6..4644ebd44d7 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs
@@ -148,6 +148,16 @@ pub(crate) fn handle_syntax_tree(
     Ok(res)
 }
 
+pub(crate) fn handle_view_syntax_tree(
+    snap: GlobalStateSnapshot,
+    params: lsp_ext::ViewSyntaxTreeParams,
+) -> anyhow::Result<String> {
+    let _p = tracing::info_span!("handle_view_syntax_tree").entered();
+    let id = from_proto::file_id(&snap, &params.text_document.uri)?;
+    let res = snap.analysis.view_syntax_tree(id)?;
+    Ok(res)
+}
+
 pub(crate) fn handle_view_hir(
     snap: GlobalStateSnapshot,
     params: lsp_types::TextDocumentPositionParams,
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/lsp/ext.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/lsp/ext.rs
index f50cbba7acf..48c2ff0a153 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/src/lsp/ext.rs
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/lsp/ext.rs
@@ -123,6 +123,20 @@ pub struct SyntaxTreeParams {
     pub range: Option<Range>,
 }
 
+pub enum ViewSyntaxTree {}
+
+impl Request for ViewSyntaxTree {
+    type Params = ViewSyntaxTreeParams;
+    type Result = String;
+    const METHOD: &'static str = "rust-analyzer/viewSyntaxTree";
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct ViewSyntaxTreeParams {
+    pub text_document: TextDocumentIdentifier,
+}
+
 pub enum ViewHir {}
 
 impl Request for ViewHir {
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs
index 97657b92658..325b0afc714 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs
+++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs
@@ -1146,6 +1146,7 @@ impl GlobalState {
             .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/src/tools/rust-analyzer/docs/dev/lsp-extensions.md b/src/tools/rust-analyzer/docs/dev/lsp-extensions.md
index 21ac3a5a269..8cd04227738 100644
--- a/src/tools/rust-analyzer/docs/dev/lsp-extensions.md
+++ b/src/tools/rust-analyzer/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
 <!---
-lsp/ext.rs hash: 6dd762ae19630ec0
+lsp/ext.rs hash: a85494375e528064
 
 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/src/tools/rust-analyzer/editors/code/package.json b/src/tools/rust-analyzer/editors/code/package.json
index e1edab4ba24..bbfe9c2a5ce 100644
--- a/src/tools/rust-analyzer/editors/code/package.json
+++ b/src/tools/rust-analyzer/editors/code/package.json
@@ -289,6 +289,24 @@
                 "category": "rust-analyzer"
             },
             {
+                "command": "rust-analyzer.syntaxTreeReveal",
+                "title": "Reveal Syntax Element",
+                "icon": "$(search)",
+                "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 +363,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,
@@ -3362,6 +3385,18 @@
                 },
                 {
                     "command": "rust-analyzer.openWalkthrough"
+                },
+                {
+                    "command": "rust-analyzer.syntaxTreeReveal",
+                    "when": "false"
+                },
+                {
+                    "command": "rust-analyzer.syntaxTreeHideWhitespace",
+                    "when": "false"
+                },
+                {
+                    "command": "rust-analyzer.syntaxTreeShowWhitespace",
+                    "when": "false"
                 }
             ],
             "editor/context": [
@@ -3375,6 +3410,25 @@
                     "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.syntaxTreeReveal",
+                    "group": "inline",
+                    "when": "view == rustSyntaxTree"
+                }
             ]
         },
         "views": {
@@ -3384,6 +3438,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/src/tools/rust-analyzer/editors/code/src/commands.ts b/src/tools/rust-analyzer/editors/code/src/commands.ts
index 64af124e726..a78696e1863 100644
--- a/src/tools/rust-analyzer/editors/code/src/commands.ts
+++ b/src/tools/rust-analyzer/editors/code/src/commands.ts
@@ -31,6 +31,7 @@ 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";
@@ -357,6 +358,38 @@ 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);
+        }
+    };
+}
+
+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;
diff --git a/src/tools/rust-analyzer/editors/code/src/config.ts b/src/tools/rust-analyzer/editors/code/src/config.ts
index 720c473c5b4..d1467a4e824 100644
--- a/src/tools/rust-analyzer/editors/code/src/config.ts
+++ b/src/tools/rust-analyzer/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/src/tools/rust-analyzer/editors/code/src/ctx.ts b/src/tools/rust-analyzer/editors/code/src/ctx.ts
index 7b5cc9f1061..5550bfa6558 100644
--- a/src/tools/rust-analyzer/editors/code/src/ctx.ts
+++ b/src/tools/rust-analyzer/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";
@@ -85,7 +86,11 @@ export class Ctx implements RustAnalyzerExtensionApi {
     private commandDisposables: Disposable[];
     private unlinkedFiles: vscode.Uri[];
     private _dependenciesProvider: RustDependenciesProvider | undefined;
-    private _dependencyTreeView: vscode.TreeView<Dependency | DependencyFile | DependencyId> | 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;
@@ -110,6 +115,14 @@ export class Ctx implements RustAnalyzerExtensionApi {
         return this._dependenciesProvider;
     }
 
+    get syntaxTreeView() {
+        return this._syntaxTreeView;
+    }
+
+    get syntaxTreeProvider() {
+        return this._syntaxTreeProvider;
+    }
+
     constructor(
         readonly extCtx: vscode.ExtensionContext,
         commandFactories: Record<string, CommandFactory>,
@@ -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) {
@@ -326,6 +342,56 @@ export class Ctx implements RustAnalyzerExtensionApi {
         );
     }
 
+    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();
@@ -424,6 +490,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
                     statusBar.command = "rust-analyzer.openLogs";
                 }
                 this.dependenciesProvider?.refresh();
+                void this.syntaxTreeProvider?.refresh();
                 break;
             case "warning":
                 statusBar.color = new vscode.ThemeColor("statusBarItem.warningForeground");
diff --git a/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts b/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts
index d52e314e219..af86d9efd14 100644
--- a/src/tools/rust-analyzer/editors/code/src/lsp_ext.ts
+++ b/src/tools/rust-analyzer/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/src/tools/rust-analyzer/editors/code/src/main.ts b/src/tools/rust-analyzer/editors/code/src/main.ts
index fdf43f66f94..989f66050ce 100644
--- a/src/tools/rust-analyzer/editors/code/src/main.ts
+++ b/src/tools/rust-analyzer/editors/code/src/main.ts
@@ -199,6 +199,9 @@ function createCommands(): Record<string, CommandFactory> {
         rename: { enabled: commands.rename },
         openLogs: { enabled: commands.openLogs },
         revealDependency: { enabled: commands.revealDependency },
+        syntaxTreeReveal: { enabled: commands.syntaxTreeReveal },
+        syntaxTreeHideWhitespace: { enabled: commands.syntaxTreeHideWhitespace },
+        syntaxTreeShowWhitespace: { enabled: commands.syntaxTreeShowWhitespace },
     };
 }
 
diff --git a/src/tools/rust-analyzer/editors/code/src/syntax_tree_provider.ts b/src/tools/rust-analyzer/editors/code/src/syntax_tree_provider.ts
new file mode 100644
index 00000000000..c7e8007e838
--- /dev/null
+++ b/src/tools/rust-analyzer/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",
+];