2019-12-30 12:05:41 -06:00
|
|
|
import * as vscode from 'vscode';
|
2020-02-24 16:57:14 -06:00
|
|
|
import * as ra from '../rust-analyzer-api';
|
2019-12-30 12:05:41 -06:00
|
|
|
|
2020-03-31 08:05:42 -05:00
|
|
|
import { Ctx, Cmd, Disposable } from '../ctx';
|
|
|
|
import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
|
|
|
|
|
|
|
|
const AST_FILE_SCHEME = "rust-analyzer";
|
2019-12-30 12:05:41 -06:00
|
|
|
|
|
|
|
// Opens the virtual file that will show the syntax tree
|
|
|
|
//
|
|
|
|
// The contents of the file come from the `TextDocumentContentProvider`
|
|
|
|
export function syntaxTree(ctx: Ctx): Cmd {
|
2019-12-30 12:12:49 -06:00
|
|
|
const tdcp = new TextDocumentContentProvider(ctx);
|
2019-12-30 12:05:41 -06:00
|
|
|
|
2020-03-31 08:05:42 -05:00
|
|
|
ctx.pushCleanup(new AstInspector);
|
|
|
|
ctx.pushCleanup(tdcp);
|
|
|
|
ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
|
2019-12-30 12:05:41 -06:00
|
|
|
|
|
|
|
return async () => {
|
|
|
|
const editor = vscode.window.activeTextEditor;
|
2020-03-31 08:05:42 -05:00
|
|
|
const rangeEnabled = !!editor && !editor.selection.isEmpty;
|
2019-12-30 12:05:41 -06:00
|
|
|
|
|
|
|
const uri = rangeEnabled
|
2019-12-30 12:12:49 -06:00
|
|
|
? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
|
|
|
|
: tdcp.uri;
|
2019-12-30 12:05:41 -06:00
|
|
|
|
|
|
|
const document = await vscode.workspace.openTextDocument(uri);
|
|
|
|
|
2019-12-30 12:12:49 -06:00
|
|
|
tdcp.eventEmitter.fire(uri);
|
2019-12-30 12:05:41 -06:00
|
|
|
|
2020-03-31 08:05:42 -05:00
|
|
|
void await vscode.window.showTextDocument(document, {
|
|
|
|
viewColumn: vscode.ViewColumn.Two,
|
|
|
|
preserveFocus: true
|
|
|
|
});
|
2019-12-30 12:05:41 -06:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-03-31 08:05:42 -05:00
|
|
|
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[] = [];
|
2019-12-30 12:05:41 -06:00
|
|
|
|
2020-02-17 07:23:23 -06:00
|
|
|
constructor(private readonly ctx: Ctx) {
|
2020-03-31 08:05:42 -05:00
|
|
|
vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables);
|
|
|
|
vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables);
|
|
|
|
}
|
|
|
|
dispose() {
|
|
|
|
this.disposables.forEach(d => d.dispose());
|
2019-12-30 12:05:41 -06:00
|
|
|
}
|
|
|
|
|
2020-03-31 08:05:42 -05:00
|
|
|
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 '';
|
2019-12-30 12:05:41 -06:00
|
|
|
|
|
|
|
// When the range based query is enabled we take the range of the selection
|
2020-03-31 08:05:42 -05:00
|
|
|
const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
|
|
|
|
? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
|
2020-02-24 16:57:14 -06:00
|
|
|
: null;
|
2019-12-30 12:05:41 -06:00
|
|
|
|
2020-03-31 08:05:42 -05:00
|
|
|
const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
|
|
|
|
return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
|
2019-12-30 12:05:41 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
get onDidChange(): vscode.Event<vscode.Uri> {
|
|
|
|
return this.eventEmitter.event;
|
|
|
|
}
|
|
|
|
}
|
2020-03-31 08:05:42 -05:00
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|