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 From 4fbca1c64df789c1fa46d083d6555b0d0b3107c0 Mon Sep 17 00:00:00 2001 From: veetaha Date: Tue, 31 Mar 2020 16:57:03 +0300 Subject: vscode: use ctx.subscriptions instead of local .disposables --- editors/code/src/commands/syntax_tree.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) (limited to 'editors/code') diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index 21ecf2661..eba511193 100644 --- a/editors/code/src/commands/syntax_tree.ts +++ b/editors/code/src/commands/syntax_tree.ts @@ -12,8 +12,8 @@ const AST_FILE_SCHEME = "rust-analyzer"; export function syntaxTree(ctx: Ctx): Cmd { const tdcp = new TextDocumentContentProvider(ctx); - ctx.pushCleanup(new AstInspector); - ctx.pushCleanup(tdcp); + void new AstInspector(ctx); + ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp)); return async () => { @@ -35,17 +35,14 @@ export function syntaxTree(ctx: Ctx): Cmd { }; } -class TextDocumentContentProvider implements vscode.TextDocumentContentProvider, Disposable { +class TextDocumentContentProvider implements vscode.TextDocumentContentProvider { 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()); + vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions); + vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions); } private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { @@ -88,16 +85,16 @@ class AstInspector implements vscode.HoverProvider, Disposable { 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); + constructor(ctx: Ctx) { + ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this)); + vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions); + vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions); + + ctx.pushCleanup(this); } dispose() { this.setRustEditor(undefined); - this.disposables.forEach(d => d.dispose()); } private onDidCloseTextDocument(doc: vscode.TextDocument) { -- cgit v1.2.3 From 3b09768ebcd7ea6523c58c92e32198ae5b18e11c Mon Sep 17 00:00:00 2001 From: Veetaha Date: Tue, 31 Mar 2020 19:06:07 +0300 Subject: vscode: apply review nits --- editors/code/src/commands/syntax_tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'editors/code') diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index eba511193..91d97f9c4 100644 --- a/editors/code/src/commands/syntax_tree.ts +++ b/editors/code/src/commands/syntax_tree.ts @@ -98,7 +98,7 @@ class AstInspector implements vscode.HoverProvider, Disposable { } private onDidCloseTextDocument(doc: vscode.TextDocument) { - if (!!this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) { + if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) { this.setRustEditor(undefined); } } -- cgit v1.2.3 From f3612b7024e5828bdd37fb6d39c1b4ebe989e819 Mon Sep 17 00:00:00 2001 From: veetaha Date: Tue, 31 Mar 2020 20:28:10 +0300 Subject: vscode: scroll to the syntax node in rust editor when highlighting --- editors/code/src/commands/syntax_tree.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'editors/code') diff --git a/editors/code/src/commands/syntax_tree.ts b/editors/code/src/commands/syntax_tree.ts index eba511193..e443c5e54 100644 --- a/editors/code/src/commands/syntax_tree.ts +++ b/editors/code/src/commands/syntax_tree.ts @@ -127,6 +127,7 @@ class AstInspector implements vscode.HoverProvider, Disposable { if (!rustTextRange) return; this.rustEditor.setDecorations(AstInspector.astDecorationType, [rustTextRange]); + this.rustEditor.revealRange(rustTextRange); const rustSourceCode = this.rustEditor.document.getText(rustTextRange); const astTextRange = this.findAstRange(astTextLine); @@ -145,7 +146,7 @@ class AstInspector implements vscode.HoverProvider, Disposable { const parsedRange = /\[(\d+); (\d+)\)/.exec(astLine); if (!parsedRange) return; - const [, begin, end] = parsedRange.map(off => doc.positionAt(+off)); + const [begin, end] = parsedRange.slice(1).map(off => doc.positionAt(+off)); return new vscode.Range(begin, end); } -- cgit v1.2.3