about summary refs log tree commit diff
path: root/src/tools/rust-analyzer/editors/code
diff options
context:
space:
mode:
authorLukas Wirth <lukastw97@gmail.com>2025-01-10 06:56:34 +0000
committerGitHub <noreply@github.com>2025-01-10 06:56:34 +0000
commit5adca85d1b2d90c8317ddd334d5dca0d7db29a68 (patch)
treece322f057e0278c6fb99c5e2e7826da5f4b92225 /src/tools/rust-analyzer/editors/code
parent669d34da4618a53ba3b6670c23820c6a8afcf04a (diff)
parent56d06fb40f1f7700ab38bf9208c01541dc560436 (diff)
downloadrust-5adca85d1b2d90c8317ddd334d5dca0d7db29a68.tar.gz
rust-5adca85d1b2d90c8317ddd334d5dca0d7db29a68.zip
Merge pull request #18813 from Giga-Bowser/syntax-tree-view
feat: Add a new and improved syntax tree view
Diffstat (limited to 'src/tools/rust-analyzer/editors/code')
-rw-r--r--src/tools/rust-analyzer/editors/code/package.json105
-rw-r--r--src/tools/rust-analyzer/editors/code/src/ast_inspector.ts216
-rw-r--r--src/tools/rust-analyzer/editors/code/src/commands.ts167
-rw-r--r--src/tools/rust-analyzer/editors/code/src/config.ts4
-rw-r--r--src/tools/rust-analyzer/editors/code/src/ctx.ts93
-rw-r--r--src/tools/rust-analyzer/editors/code/src/lsp_ext.ts4
-rw-r--r--src/tools/rust-analyzer/editors/code/src/main.ts5
-rw-r--r--src/tools/rust-analyzer/editors/code/src/syntax_tree_provider.ts301
8 files changed, 555 insertions, 340 deletions
diff --git a/src/tools/rust-analyzer/editors/code/package.json b/src/tools/rust-analyzer/editors/code/package.json
index 62ef84e9946..26cd49d9d2a 100644
--- a/src/tools/rust-analyzer/editors/code/package.json
+++ b/src/tools/rust-analyzer/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/src/tools/rust-analyzer/editors/code/src/ast_inspector.ts b/src/tools/rust-analyzer/editors/code/src/ast_inspector.ts
deleted file mode 100644
index 35b705c477e..00000000000
--- a/src/tools/rust-analyzer/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/src/tools/rust-analyzer/editors/code/src/commands.ts b/src/tools/rust-analyzer/editors/code/src/commands.ts
index 73e39c900e7..b3aa04af7ed 100644
--- a/src/tools/rust-analyzer/editors/code/src/commands.ts
+++ b/src/tools/rust-analyzer/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/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 37a54abf71f..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";
@@ -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/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..c84b69b66cd 100644
--- a/src/tools/rust-analyzer/editors/code/src/main.ts
+++ b/src/tools/rust-analyzer/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/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",
+];