From 09a760e52e20dcd79d902b05065934615cc4d56b Mon Sep 17 00:00:00 2001 From: veetaha Date: Tue, 31 Mar 2020 16:05:42 +0300 Subject: vscode: add syntax tree inspection hovers and highlights --- editors/code/src/commands/syntax_tree.ts | 169 +++++++++++++++++++++---------- editors/code/src/util.ts | 4 +- 2 files changed, 118 insertions(+), 55 deletions(-) (limited to 'editors/code') diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index 2e08e8f11..21ecf2661 100644 --- a/editors/code/src/commands/syntax_tree.ts +++ b/editors/code/src/commands/syntax_tree.ts @@ -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(); +class TextDocumentContentProvider implements vscode.TextDocumentContentProvider, Disposable { + readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree'); + readonly eventEmitter = new vscode.EventEmitter(); + 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 { - 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 { + 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 { 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 { + 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); + } +} diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index 978a31751..6f91f81d6 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -65,12 +65,12 @@ export async function sendRequestWithRetry( 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' -- cgit v1.2.3