mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-10-29 10:58:02 +00:00
Add a new and improved syntax tree viewer
This commit is contained in:
parent
c0eaff7dd1
commit
5ffe45d8cd
14 changed files with 759 additions and 4 deletions
|
|
@ -31,6 +31,7 @@ import type { LanguageClient } from "vscode-languageclient/node";
|
|||
import { HOVER_REFERENCE_COMMAND } from "./client";
|
||||
import type { DependencyId } from "./dependencies_provider";
|
||||
import { log } from "./util";
|
||||
import type { SyntaxElement } from "./syntax_tree_provider";
|
||||
|
||||
export * from "./ast_inspector";
|
||||
export * from "./run";
|
||||
|
|
@ -357,6 +358,38 @@ export async function execRevealDependency(e: RustEditor): Promise<void> {
|
|||
await vscode.commands.executeCommand("rust-analyzer.revealDependency", e);
|
||||
}
|
||||
|
||||
export function syntaxTreeReveal(): Cmd {
|
||||
return async (element: SyntaxElement) => {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
|
||||
if (activeEditor !== undefined) {
|
||||
const start = activeEditor.document.positionAt(element.start);
|
||||
const end = activeEditor.document.positionAt(element.end);
|
||||
|
||||
const newSelection = new vscode.Selection(start, end);
|
||||
|
||||
activeEditor.selection = newSelection;
|
||||
activeEditor.revealRange(newSelection);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function syntaxTreeHideWhitespace(ctx: CtxInit): Cmd {
|
||||
return async () => {
|
||||
if (ctx.syntaxTreeProvider !== undefined) {
|
||||
await ctx.syntaxTreeProvider.toggleWhitespace();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function syntaxTreeShowWhitespace(ctx: CtxInit): Cmd {
|
||||
return async () => {
|
||||
if (ctx.syntaxTreeProvider !== undefined) {
|
||||
await ctx.syntaxTreeProvider.toggleWhitespace();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function ssr(ctx: CtxInit): Cmd {
|
||||
return async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
RustDependenciesProvider,
|
||||
type DependencyId,
|
||||
} from "./dependencies_provider";
|
||||
import { SyntaxTreeProvider, type SyntaxElement } from "./syntax_tree_provider";
|
||||
import { execRevealDependency } from "./commands";
|
||||
import { PersistentState } from "./persistent_state";
|
||||
import { bootstrap } from "./bootstrap";
|
||||
|
|
@ -85,7 +86,11 @@ export class Ctx implements RustAnalyzerExtensionApi {
|
|||
private commandDisposables: Disposable[];
|
||||
private unlinkedFiles: vscode.Uri[];
|
||||
private _dependenciesProvider: RustDependenciesProvider | undefined;
|
||||
private _dependencyTreeView: vscode.TreeView<Dependency | DependencyFile | DependencyId> | undefined;
|
||||
private _dependencyTreeView:
|
||||
| vscode.TreeView<Dependency | DependencyFile | DependencyId>
|
||||
| undefined;
|
||||
private _syntaxTreeProvider: SyntaxTreeProvider | undefined;
|
||||
private _syntaxTreeView: vscode.TreeView<SyntaxElement> | undefined;
|
||||
private lastStatus: ServerStatusParams | { health: "stopped" } = { health: "stopped" };
|
||||
private _serverVersion: string;
|
||||
private statusBarActiveEditorListener: Disposable;
|
||||
|
|
@ -110,6 +115,14 @@ export class Ctx implements RustAnalyzerExtensionApi {
|
|||
return this._dependenciesProvider;
|
||||
}
|
||||
|
||||
get syntaxTreeView() {
|
||||
return this._syntaxTreeView;
|
||||
}
|
||||
|
||||
get syntaxTreeProvider() {
|
||||
return this._syntaxTreeProvider;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly extCtx: vscode.ExtensionContext,
|
||||
commandFactories: Record<string, CommandFactory>,
|
||||
|
|
@ -278,6 +291,9 @@ export class Ctx implements RustAnalyzerExtensionApi {
|
|||
if (this.config.showDependenciesExplorer) {
|
||||
this.prepareTreeDependenciesView(client);
|
||||
}
|
||||
if (this.config.showSyntaxTree) {
|
||||
this.prepareSyntaxTreeView(client);
|
||||
}
|
||||
}
|
||||
|
||||
private prepareTreeDependenciesView(client: lc.LanguageClient) {
|
||||
|
|
@ -326,6 +342,56 @@ export class Ctx implements RustAnalyzerExtensionApi {
|
|||
);
|
||||
}
|
||||
|
||||
private prepareSyntaxTreeView(client: lc.LanguageClient) {
|
||||
const ctxInit: CtxInit = {
|
||||
...this,
|
||||
client: client,
|
||||
};
|
||||
this._syntaxTreeProvider = new SyntaxTreeProvider(ctxInit);
|
||||
this._syntaxTreeView = vscode.window.createTreeView("rustSyntaxTree", {
|
||||
treeDataProvider: this._syntaxTreeProvider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
|
||||
this.pushExtCleanup(this._syntaxTreeView);
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(async () => {
|
||||
if (this.syntaxTreeView?.visible) {
|
||||
await this.syntaxTreeProvider?.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument(async () => {
|
||||
if (this.syntaxTreeView?.visible) {
|
||||
await this.syntaxTreeProvider?.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
vscode.window.onDidChangeTextEditorSelection(async (e) => {
|
||||
if (!this.syntaxTreeView?.visible || !isRustEditor(e.textEditor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = e.selections[0];
|
||||
if (selection === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = e.textEditor.document.offsetAt(selection.start);
|
||||
const end = e.textEditor.document.offsetAt(selection.end);
|
||||
const result = this.syntaxTreeProvider?.getElementByRange(start, end);
|
||||
if (result !== undefined) {
|
||||
await this.syntaxTreeView?.reveal(result);
|
||||
}
|
||||
});
|
||||
|
||||
this._syntaxTreeView.onDidChangeVisibility(async (e) => {
|
||||
if (e.visible) {
|
||||
await this.syntaxTreeProvider?.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async restart() {
|
||||
// FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
|
||||
await this.stopAndDispose();
|
||||
|
|
@ -424,6 +490,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
|
|||
statusBar.command = "rust-analyzer.openLogs";
|
||||
}
|
||||
this.dependenciesProvider?.refresh();
|
||||
void this.syntaxTreeProvider?.refresh();
|
||||
break;
|
||||
case "warning":
|
||||
statusBar.color = new vscode.ThemeColor("statusBarItem.warningForeground");
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,9 @@ function createCommands(): Record<string, CommandFactory> {
|
|||
rename: { enabled: commands.rename },
|
||||
openLogs: { enabled: commands.openLogs },
|
||||
revealDependency: { enabled: commands.revealDependency },
|
||||
syntaxTreeReveal: { enabled: commands.syntaxTreeReveal },
|
||||
syntaxTreeHideWhitespace: { enabled: commands.syntaxTreeHideWhitespace },
|
||||
syntaxTreeShowWhitespace: { enabled: commands.syntaxTreeShowWhitespace },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
301
editors/code/src/syntax_tree_provider.ts
Normal file
301
editors/code/src/syntax_tree_provider.ts
Normal 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",
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue