Skip to content

Commit 09a760e

Browse files
committed
vscode: add syntax tree inspection hovers and highlights
1 parent 6f0d8db commit 09a760e

File tree

2 files changed

+118
-55
lines changed

2 files changed

+118
-55
lines changed
Lines changed: 116 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,24 @@
11
import * as vscode from 'vscode';
22
import * as ra from '../rust-analyzer-api';
33

4-
import { Ctx, Cmd } from '../ctx';
5-
import { isRustDocument } from '../util';
4+
import { Ctx, Cmd, Disposable } from '../ctx';
5+
import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
6+
7+
const AST_FILE_SCHEME = "rust-analyzer";
68

79
// Opens the virtual file that will show the syntax tree
810
//
911
// The contents of the file come from the `TextDocumentContentProvider`
1012
export function syntaxTree(ctx: Ctx): Cmd {
1113
const tdcp = new TextDocumentContentProvider(ctx);
1214

13-
ctx.pushCleanup(
14-
vscode.workspace.registerTextDocumentContentProvider(
15-
'rust-analyzer',
16-
tdcp,
17-
),
18-
);
19-
20-
vscode.workspace.onDidChangeTextDocument(
21-
(event: vscode.TextDocumentChangeEvent) => {
22-
const doc = event.document;
23-
if (!isRustDocument(doc)) return;
24-
afterLs(() => tdcp.eventEmitter.fire(tdcp.uri));
25-
},
26-
null,
27-
ctx.subscriptions,
28-
);
29-
30-
vscode.window.onDidChangeActiveTextEditor(
31-
(editor: vscode.TextEditor | undefined) => {
32-
if (!editor || !isRustDocument(editor.document)) return;
33-
tdcp.eventEmitter.fire(tdcp.uri);
34-
},
35-
null,
36-
ctx.subscriptions,
37-
);
15+
ctx.pushCleanup(new AstInspector);
16+
ctx.pushCleanup(tdcp);
17+
ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
3818

3919
return async () => {
4020
const editor = vscode.window.activeTextEditor;
41-
const rangeEnabled = !!(editor && !editor.selection.isEmpty);
21+
const rangeEnabled = !!editor && !editor.selection.isEmpty;
4222

4323
const uri = rangeEnabled
4424
? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
@@ -48,45 +28,128 @@ export function syntaxTree(ctx: Ctx): Cmd {
4828

4929
tdcp.eventEmitter.fire(uri);
5030

51-
return vscode.window.showTextDocument(
52-
document,
53-
vscode.ViewColumn.Two,
54-
true,
55-
);
31+
void await vscode.window.showTextDocument(document, {
32+
viewColumn: vscode.ViewColumn.Two,
33+
preserveFocus: true
34+
});
5635
};
5736
}
5837

59-
// We need to order this after LS updates, but there's no API for that.
60-
// Hence, good old setTimeout.
61-
function afterLs(f: () => void) {
62-
setTimeout(f, 10);
63-
}
64-
65-
66-
class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
67-
uri = vscode.Uri.parse('rust-analyzer://syntaxtree');
68-
eventEmitter = new vscode.EventEmitter<vscode.Uri>();
38+
class TextDocumentContentProvider implements vscode.TextDocumentContentProvider, Disposable {
39+
readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree');
40+
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
41+
private readonly disposables: Disposable[] = [];
6942

7043
constructor(private readonly ctx: Ctx) {
44+
vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables);
45+
vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables);
46+
}
47+
dispose() {
48+
this.disposables.forEach(d => d.dispose());
7149
}
7250

73-
provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> {
74-
const editor = vscode.window.activeTextEditor;
75-
const client = this.ctx.client;
76-
if (!editor || !client) return '';
51+
private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
52+
if (isRustDocument(event.document)) {
53+
// We need to order this after language server updates, but there's no API for that.
54+
// Hence, good old sleep().
55+
void sleep(10).then(() => this.eventEmitter.fire(this.uri));
56+
}
57+
}
58+
private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
59+
if (editor && isRustEditor(editor)) {
60+
this.eventEmitter.fire(this.uri);
61+
}
62+
}
63+
64+
provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
65+
const rustEditor = this.ctx.activeRustEditor;
66+
if (!rustEditor) return '';
7767

7868
// When the range based query is enabled we take the range of the selection
79-
const range = uri.query === 'range=true' && !editor.selection.isEmpty
80-
? client.code2ProtocolConverter.asRange(editor.selection)
69+
const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
70+
? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
8171
: null;
8272

83-
return client.sendRequest(ra.syntaxTree, {
84-
textDocument: { uri: editor.document.uri.toString() },
85-
range,
86-
});
73+
const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
74+
return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
8775
}
8876

8977
get onDidChange(): vscode.Event<vscode.Uri> {
9078
return this.eventEmitter.event;
9179
}
9280
}
81+
82+
83+
// FIXME: consider implementing this via the Tree View API?
84+
// https://code.visualstudio.com/api/extension-guides/tree-view
85+
class AstInspector implements vscode.HoverProvider, Disposable {
86+
private static readonly astDecorationType = vscode.window.createTextEditorDecorationType({
87+
fontStyle: "normal",
88+
border: "#ffffff 1px solid",
89+
});
90+
private rustEditor: undefined | RustEditor;
91+
private readonly disposables: Disposable[] = [];
92+
93+
constructor() {
94+
this.disposables.push(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this));
95+
vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this.disposables);
96+
vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
97+
}
98+
dispose() {
99+
this.setRustEditor(undefined);
100+
this.disposables.forEach(d => d.dispose());
101+
}
102+
103+
private onDidCloseTextDocument(doc: vscode.TextDocument) {
104+
if (!!this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
105+
this.setRustEditor(undefined);
106+
}
107+
}
108+
109+
private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
110+
if (editors.every(suspect => suspect.document.uri.scheme !== AST_FILE_SCHEME)) {
111+
this.setRustEditor(undefined);
112+
return;
113+
}
114+
this.setRustEditor(editors.find(isRustEditor));
115+
}
116+
117+
private setRustEditor(newRustEditor: undefined | RustEditor) {
118+
if (newRustEditor !== this.rustEditor) {
119+
this.rustEditor?.setDecorations(AstInspector.astDecorationType, []);
120+
}
121+
this.rustEditor = newRustEditor;
122+
}
123+
124+
provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
125+
if (!this.rustEditor) return;
126+
127+
const astTextLine = doc.lineAt(hoverPosition.line);
128+
129+
const rustTextRange = this.parseRustTextRange(this.rustEditor.document, astTextLine.text);
130+
if (!rustTextRange) return;
131+
132+
this.rustEditor.setDecorations(AstInspector.astDecorationType, [rustTextRange]);
133+
134+
const rustSourceCode = this.rustEditor.document.getText(rustTextRange);
135+
const astTextRange = this.findAstRange(astTextLine);
136+
137+
return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astTextRange);
138+
}
139+
140+
private findAstRange(astLine: vscode.TextLine) {
141+
const lineOffset = astLine.range.start;
142+
const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
143+
const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
144+
return new vscode.Range(begin, end);
145+
}
146+
147+
private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
148+
const parsedRange = /\[(\d+); (\d+)\)/.exec(astLine);
149+
if (!parsedRange) return;
150+
151+
const [, begin, end] = parsedRange.map(off => doc.positionAt(+off));
152+
153+
return new vscode.Range(begin, end);
154+
}
155+
}

editors/code/src/util.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ export async function sendRequestWithRetry<TParam, TRet>(
6565
throw 'unreachable';
6666
}
6767

68-
function sleep(ms: number) {
68+
export function sleep(ms: number) {
6969
return new Promise(resolve => setTimeout(resolve, ms));
7070
}
7171

7272
export type RustDocument = vscode.TextDocument & { languageId: "rust" };
73-
export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string };
73+
export type RustEditor = vscode.TextEditor & { document: RustDocument };
7474

7575
export function isRustDocument(document: vscode.TextDocument): document is RustDocument {
7676
return document.languageId === 'rust'

0 commit comments

Comments
 (0)