Merge pull request #18813 from Giga-Bowser/syntax-tree-view

feat: Add a new and improved syntax tree view
This commit is contained in:
Lukas Wirth 2025-01-10 06:56:34 +00:00 committed by GitHub
commit d133136bc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 812 additions and 701 deletions

View file

@ -108,11 +108,6 @@
}
],
"commands": [
{
"command": "rust-analyzer.syntaxTree",
"title": "Show Syntax Tree",
"category": "rust-analyzer (debug command)"
},
{
"command": "rust-analyzer.viewHir",
"title": "View Hir",
@ -288,6 +283,30 @@
"title": "Reveal File",
"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",
@ -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",
@ -3274,10 +3287,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": [

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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");
}

View file

@ -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");

View file

@ -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 };

View file

@ -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 },
};
}

View file

@ -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",
];