diff options
| author | Lukas Wirth <lukastw97@gmail.com> | 2025-01-10 06:56:34 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-10 06:56:34 +0000 |
| commit | 5adca85d1b2d90c8317ddd334d5dca0d7db29a68 (patch) | |
| tree | ce322f057e0278c6fb99c5e2e7826da5f4b92225 /src/tools/rust-analyzer/editors/code | |
| parent | 669d34da4618a53ba3b6670c23820c6a8afcf04a (diff) | |
| parent | 56d06fb40f1f7700ab38bf9208c01541dc560436 (diff) | |
| download | rust-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')
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", +]; |
