vscode: add syntax tree inspection hovers and highlights

This commit is contained in:
veetaha 2020-03-31 16:05:42 +03:00
parent 6f0d8db529
commit 09a760e52e
2 changed files with 118 additions and 55 deletions

View file

@ -1,8 +1,10 @@
import * as vscode from 'vscode';
import * as ra from '../rust-analyzer-api';
import { Ctx, Cmd } from '../ctx';
import { isRustDocument } from '../util';
import { Ctx, Cmd, Disposable } from '../ctx';
import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
const AST_FILE_SCHEME = "rust-analyzer";
// Opens the virtual file that will show the syntax tree
//
@ -10,35 +12,13 @@ import { isRustDocument } from '../util';
export function syntaxTree(ctx: Ctx): Cmd {
const tdcp = new TextDocumentContentProvider(ctx);
ctx.pushCleanup(
vscode.workspace.registerTextDocumentContentProvider(
'rust-analyzer',
tdcp,
),
);
vscode.workspace.onDidChangeTextDocument(
(event: vscode.TextDocumentChangeEvent) => {
const doc = event.document;
if (!isRustDocument(doc)) return;
afterLs(() => tdcp.eventEmitter.fire(tdcp.uri));
},
null,
ctx.subscriptions,
);
vscode.window.onDidChangeActiveTextEditor(
(editor: vscode.TextEditor | undefined) => {
if (!editor || !isRustDocument(editor.document)) return;
tdcp.eventEmitter.fire(tdcp.uri);
},
null,
ctx.subscriptions,
);
ctx.pushCleanup(new AstInspector);
ctx.pushCleanup(tdcp);
ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
return async () => {
const editor = vscode.window.activeTextEditor;
const rangeEnabled = !!(editor && !editor.selection.isEmpty);
const rangeEnabled = !!editor && !editor.selection.isEmpty;
const uri = rangeEnabled
? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
@ -48,45 +28,128 @@ export function syntaxTree(ctx: Ctx): Cmd {
tdcp.eventEmitter.fire(uri);
return vscode.window.showTextDocument(
document,
vscode.ViewColumn.Two,
true,
);
void await vscode.window.showTextDocument(document, {
viewColumn: vscode.ViewColumn.Two,
preserveFocus: true
});
};
}
// We need to order this after LS updates, but there's no API for that.
// Hence, good old setTimeout.
function afterLs(f: () => void) {
setTimeout(f, 10);
}
class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
uri = vscode.Uri.parse('rust-analyzer://syntaxtree');
eventEmitter = new vscode.EventEmitter<vscode.Uri>();
class TextDocumentContentProvider implements vscode.TextDocumentContentProvider, Disposable {
readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree');
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
private readonly disposables: Disposable[] = [];
constructor(private readonly ctx: Ctx) {
vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables);
vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables);
}
dispose() {
this.disposables.forEach(d => d.dispose());
}
provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> {
const editor = vscode.window.activeTextEditor;
const client = this.ctx.client;
if (!editor || !client) return '';
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);
}
}
provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
const rustEditor = this.ctx.activeRustEditor;
if (!rustEditor) return '';
// When the range based query is enabled we take the range of the selection
const range = uri.query === 'range=true' && !editor.selection.isEmpty
? client.code2ProtocolConverter.asRange(editor.selection)
const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
: null;
return client.sendRequest(ra.syntaxTree, {
textDocument: { uri: editor.document.uri.toString() },
range,
});
const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
}
get onDidChange(): vscode.Event<vscode.Uri> {
return this.eventEmitter.event;
}
}
// FIXME: consider implementing this via the Tree View API?
// https://code.visualstudio.com/api/extension-guides/tree-view
class AstInspector implements vscode.HoverProvider, Disposable {
private static readonly astDecorationType = vscode.window.createTextEditorDecorationType({
fontStyle: "normal",
border: "#ffffff 1px solid",
});
private rustEditor: undefined | RustEditor;
private readonly disposables: Disposable[] = [];
constructor() {
this.disposables.push(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this));
vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this.disposables);
vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
}
dispose() {
this.setRustEditor(undefined);
this.disposables.forEach(d => d.dispose());
}
private onDidCloseTextDocument(doc: vscode.TextDocument) {
if (!!this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
this.setRustEditor(undefined);
}
}
private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
if (editors.every(suspect => suspect.document.uri.scheme !== AST_FILE_SCHEME)) {
this.setRustEditor(undefined);
return;
}
this.setRustEditor(editors.find(isRustEditor));
}
private setRustEditor(newRustEditor: undefined | RustEditor) {
if (newRustEditor !== this.rustEditor) {
this.rustEditor?.setDecorations(AstInspector.astDecorationType, []);
}
this.rustEditor = newRustEditor;
}
provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
if (!this.rustEditor) return;
const astTextLine = doc.lineAt(hoverPosition.line);
const rustTextRange = this.parseRustTextRange(this.rustEditor.document, astTextLine.text);
if (!rustTextRange) return;
this.rustEditor.setDecorations(AstInspector.astDecorationType, [rustTextRange]);
const rustSourceCode = this.rustEditor.document.getText(rustTextRange);
const astTextRange = this.findAstRange(astTextLine);
return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astTextRange);
}
private findAstRange(astLine: vscode.TextLine) {
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.map(off => doc.positionAt(+off));
return new vscode.Range(begin, end);
}
}

View file

@ -65,12 +65,12 @@ export async function sendRequestWithRetry<TParam, TRet>(
throw 'unreachable';
}
function sleep(ms: number) {
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export type RustDocument = vscode.TextDocument & { languageId: "rust" };
export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string };
export type RustEditor = vscode.TextEditor & { document: RustDocument };
export function isRustDocument(document: vscode.TextDocument): document is RustDocument {
return document.languageId === 'rust'