From 0e6d066a2940c65af9171dff52304590cac4b95e Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 29 Feb 2020 19:27:52 +0200 Subject: [PATCH 01/10] vscode: extract Type and Param hint cases of InlayHint at type level (needed further) --- editors/code/src/rust-analyzer-api.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts index c5a010e947..6a7aeb6022 100644 --- a/editors/code/src/rust-analyzer-api.ts +++ b/editors/code/src/rust-analyzer-api.ts @@ -86,14 +86,20 @@ export interface Runnable { export const runnables = request>("runnables"); -export const enum InlayKind { - TypeHint = "TypeHint", - ParameterHint = "ParameterHint", -} -export interface InlayHint { - range: lc.Range; - kind: InlayKind; - label: string; + +export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint; + +export namespace InlayHint { + export const enum Kind { + TypeHint = "TypeHint", + ParamHint = "ParameterHint", + } + interface Common { + range: lc.Range; + label: string; + } + export type TypeHint = Common & { kind: Kind.TypeHint; } + export type ParamHint = Common & { kind: Kind.ParamHint; } } export interface InlayHintsParams { textDocument: lc.TextDocumentIdentifier; From 6441988d84cc1f9d347d72a48d2b67b19dcb8cc9 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 29 Feb 2020 19:28:26 +0200 Subject: [PATCH 02/10] vscode: redesign inlay hints to be capable of handling multiple editors --- editors/code/src/inlay_hints.ts | 373 +++++++++++++++++++------- editors/code/src/rust-analyzer-api.ts | 4 +- 2 files changed, 272 insertions(+), 105 deletions(-) diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index 08d3a64a77..bbe8f67ef5 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -1,156 +1,323 @@ +import * as lc from "vscode-languageclient"; import * as vscode from 'vscode'; import * as ra from './rust-analyzer-api'; import { Ctx } from './ctx'; -import { log, sendRequestWithRetry, isRustDocument } from './util'; +import { sendRequestWithRetry, assert } from './util'; export function activateInlayHints(ctx: Ctx) { - const hintsUpdater = new HintsUpdater(ctx); + const hintsUpdater = new HintsUpdater(ctx.client); + vscode.window.onDidChangeVisibleTextEditors( - async _ => hintsUpdater.refresh(), + visibleEditors => hintsUpdater.refreshVisibleRustEditors( + visibleEditors.filter(isRustTextEditor) + ), null, ctx.subscriptions ); vscode.workspace.onDidChangeTextDocument( - async event => { - if (event.contentChanges.length === 0) return; - if (!isRustDocument(event.document)) return; - await hintsUpdater.refresh(); + ({ contentChanges, document }) => { + if (contentChanges.length === 0) return; + if (!isRustTextDocument(document)) return; + + hintsUpdater.refreshRustDocument(document); }, null, ctx.subscriptions ); vscode.workspace.onDidChangeConfiguration( - async _ => hintsUpdater.setEnabled(ctx.config.displayInlayHints), + async _ => { + // FIXME: ctx.config may have not been refreshed at this point of time, i.e. + // it's on onDidChangeConfiguration() handler may've not executed yet + // (order of invokation is unspecified) + // To fix this we should expose an event emitter from our `Config` itself. + await hintsUpdater.setEnabled(ctx.config.displayInlayHints); + }, null, ctx.subscriptions ); ctx.pushCleanup({ dispose() { - hintsUpdater.clear(); + hintsUpdater.clearHints(); } }); - // XXX: we don't await this, thus Promise rejections won't be handled, but - // this should never throw in fact... - void hintsUpdater.setEnabled(ctx.config.displayInlayHints); + hintsUpdater.setEnabled(ctx.config.displayInlayHints); } -const typeHintDecorationType = vscode.window.createTextEditorDecorationType({ - after: { - color: new vscode.ThemeColor('rust_analyzer.inlayHint'), - fontStyle: "normal", - }, -}); -const parameterHintDecorationType = vscode.window.createTextEditorDecorationType({ - before: { - color: new vscode.ThemeColor('rust_analyzer.inlayHint'), - fontStyle: "normal", - }, -}); +const typeHints = { + decorationType: vscode.window.createTextEditorDecorationType({ + after: { + color: new vscode.ThemeColor('rust_analyzer.inlayHint'), + fontStyle: "normal", + } + }), + + toDecoration(hint: ra.InlayHint.TypeHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions { + return { + range: conv.asRange(hint.range), + renderOptions: { after: { contentText: `: ${hint.label}` } } + }; + } +}; + +const paramHints = { + decorationType: vscode.window.createTextEditorDecorationType({ + before: { + color: new vscode.ThemeColor('rust_analyzer.inlayHint'), + fontStyle: "normal", + } + }), + + toDecoration(hint: ra.InlayHint.ParamHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions { + return { + range: conv.asRange(hint.range), + renderOptions: { before: { contentText: `${hint.label}: ` } } + }; + } +}; class HintsUpdater { - private pending = new Map(); - private ctx: Ctx; - private enabled: boolean; + private sourceFiles = new RustSourceFiles(); + private enabled = false; - constructor(ctx: Ctx) { - this.ctx = ctx; - this.enabled = false; - } - - async setEnabled(enabled: boolean): Promise { - log.debug({ enabled, prev: this.enabled }); + constructor(readonly client: lc.LanguageClient) { } + setEnabled(enabled: boolean) { if (this.enabled === enabled) return; this.enabled = enabled; if (this.enabled) { - return await this.refresh(); + this.refreshVisibleRustEditors(vscode.window.visibleTextEditors.filter(isRustTextEditor)); } else { - return this.clear(); + this.clearHints(); } } - clear() { - this.ctx.visibleRustEditors.forEach(it => { - this.setTypeDecorations(it, []); - this.setParameterDecorations(it, []); - }); + clearHints() { + for (const file of this.sourceFiles) { + file.inlaysRequest?.cancel(); + this.renderHints(file, []); + } } - async refresh() { + private renderHints(file: RustSourceFile, hints: ra.InlayHint[]) { + file.renderHints(hints, this.client.protocol2CodeConverter); + } + + refreshRustDocument(document: RustTextDocument) { if (!this.enabled) return; - await Promise.all(this.ctx.visibleRustEditors.map(it => this.refreshEditor(it))); + + const file = this.sourceFiles.getSourceFile(document.uri.toString()); + + assert(!!file, "Document must be opened in some text editor!"); + + void file.fetchAndRenderHints(this.client); } - private async refreshEditor(editor: vscode.TextEditor): Promise { - const newHints = await this.queryHints(editor.document.uri.toString()); - if (newHints == null) return; + refreshVisibleRustEditors(visibleEditors: RustTextEditor[]) { + if (!this.enabled) return; - const newTypeDecorations = newHints - .filter(hint => hint.kind === ra.InlayKind.TypeHint) - .map(hint => ({ - range: this.ctx.client.protocol2CodeConverter.asRange(hint.range), - renderOptions: { - after: { - contentText: `: ${hint.label}`, - }, - }, - })); - this.setTypeDecorations(editor, newTypeDecorations); + const visibleSourceFiles = this.sourceFiles.drainEditors(visibleEditors); - const newParameterDecorations = newHints - .filter(hint => hint.kind === ra.InlayKind.ParameterHint) - .map(hint => ({ - range: this.ctx.client.protocol2CodeConverter.asRange(hint.range), - renderOptions: { - before: { - contentText: `${hint.label}: `, - }, - }, - })); - this.setParameterDecorations(editor, newParameterDecorations); - } + // Cancel requests for source files whose editors were disposed (leftovers after drain). + for (const { inlaysRequest } of this.sourceFiles) inlaysRequest?.cancel(); - private setTypeDecorations( - editor: vscode.TextEditor, - decorations: vscode.DecorationOptions[], - ) { - editor.setDecorations( - typeHintDecorationType, - this.enabled ? decorations : [], - ); - } + this.sourceFiles = visibleSourceFiles; - private setParameterDecorations( - editor: vscode.TextEditor, - decorations: vscode.DecorationOptions[], - ) { - editor.setDecorations( - parameterHintDecorationType, - this.enabled ? decorations : [], - ); - } - - private async queryHints(documentUri: string): Promise { - this.pending.get(documentUri)?.cancel(); - - const tokenSource = new vscode.CancellationTokenSource(); - this.pending.set(documentUri, tokenSource); - - const request = { textDocument: { uri: documentUri } }; - - return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token) - .catch(_ => null) - .finally(() => { - if (!tokenSource.token.isCancellationRequested) { - this.pending.delete(documentUri); - } - }); + for (const file of this.sourceFiles) { + if (!file.rerenderHints()) { + void file.fetchAndRenderHints(this.client); + } + } } } + + +/** + * This class encapsulates a map of file uris to respective inlay hints + * request cancellation token source (cts) and an array of editors. + * E.g. + * ``` + * { + * file1.rs -> (cts, (typeDecor, paramDecor), [editor1, editor2]) + * ^-- there is a cts to cancel the in-flight request + * file2.rs -> (cts, null, [editor3]) + * ^-- no decorations are applied to this source file yet + * file3.rs -> (null, (typeDecor, paramDecor), [editor4]) + * } ^-- there is no inflight request + * ``` + * + * Invariants: each stored source file has at least 1 editor. + */ +class RustSourceFiles { + private files = new Map(); + + /** + * Removes `editors` from `this` source files and puts them into a returned + * source files object. cts and decorations are moved to the returned source files. + */ + drainEditors(editors: RustTextEditor[]): RustSourceFiles { + const result = new RustSourceFiles; + + for (const editor of editors) { + const oldFile = this.removeEditor(editor); + const newFile = result.addEditor(editor); + + if (oldFile) newFile.stealCacheFrom(oldFile); + } + + return result; + } + + /** + * Remove the editor and if it was the only editor for a source file, + * the source file is removed altogether. + * + * @returns A reference to the source file for this editor or + * null if no such source file was not found. + */ + private removeEditor(editor: RustTextEditor): null | RustSourceFile { + const uri = editor.document.uri.toString(); + + const file = this.files.get(uri); + if (!file) return null; + + const editorIndex = file.editors.findIndex(suspect => areEditorsEqual(suspect, editor)); + + if (editorIndex >= 0) { + file.editors.splice(editorIndex, 1); + + if (file.editors.length === 0) this.files.delete(uri); + } + + return file; + } + + /** + * @returns A reference to an existing source file or newly created one for the editor. + */ + private addEditor(editor: RustTextEditor): RustSourceFile { + const uri = editor.document.uri.toString(); + const file = this.files.get(uri); + + if (!file) { + const newFile = new RustSourceFile([editor]); + this.files.set(uri, newFile); + return newFile; + } + + if (!file.editors.find(suspect => areEditorsEqual(suspect, editor))) { + file.editors.push(editor); + } + return file; + } + + getSourceFile(uri: string): undefined | RustSourceFile { + return this.files.get(uri); + } + + [Symbol.iterator](): IterableIterator { + return this.files.values(); + } +} +class RustSourceFile { + constructor( + /** + * Editors for this source file (one text document may be opened in multiple editors). + * We keep this just an array, because most of the time we have 1 editor for 1 source file. + */ + readonly editors: RustTextEditor[], + /** + * Source of the token to cancel in-flight inlay hints request if any. + */ + public inlaysRequest: null | vscode.CancellationTokenSource = null, + + public decorations: null | { + type: vscode.DecorationOptions[]; + param: vscode.DecorationOptions[]; + } = null + ) { } + + stealCacheFrom(other: RustSourceFile) { + if (other.inlaysRequest) this.inlaysRequest = other.inlaysRequest; + if (other.decorations) this.decorations = other.decorations; + + other.inlaysRequest = null; + other.decorations = null; + } + + rerenderHints(): boolean { + if (!this.decorations) return false; + + for (const editor of this.editors) { + editor.setDecorations(typeHints.decorationType, this.decorations.type); + editor.setDecorations(paramHints.decorationType, this.decorations.param); + } + return true; + } + + renderHints(hints: ra.InlayHint[], conv: lc.Protocol2CodeConverter) { + this.decorations = { type: [], param: [] }; + + for (const hint of hints) { + switch (hint.kind) { + case ra.InlayHint.Kind.TypeHint: { + this.decorations.type.push(typeHints.toDecoration(hint, conv)); + continue; + } + case ra.InlayHint.Kind.ParamHint: { + this.decorations.param.push(paramHints.toDecoration(hint, conv)); + continue; + } + } + } + this.rerenderHints(); + } + + async fetchAndRenderHints(client: lc.LanguageClient): Promise { + this.inlaysRequest?.cancel(); + + const tokenSource = new vscode.CancellationTokenSource(); + this.inlaysRequest = tokenSource; + + const request = { textDocument: { uri: this.editors[0].document.uri.toString() } }; + + try { + const hints = await sendRequestWithRetry(client, ra.inlayHints, request, tokenSource.token); + this.renderHints(hints, client.protocol2CodeConverter); + } catch { + /* ignore */ + } finally { + if (this.inlaysRequest === tokenSource) { + this.inlaysRequest = null; + } + } + } +} + +type RustTextDocument = vscode.TextDocument & { languageId: "rust" }; +type RustTextEditor = vscode.TextEditor & { document: RustTextDocument; id: string }; + +function areEditorsEqual(a: RustTextEditor, b: RustTextEditor): boolean { + return a.id === b.id; +} + +function isRustTextEditor(suspect: vscode.TextEditor & { id?: unknown }): suspect is RustTextEditor { + // Dirty hack, we need to access private vscode editor id, + // see https://github.com/microsoft/vscode/issues/91788 + assert( + typeof suspect.id === "string", + "Private text editor id is no longer available, please update the workaround!" + ); + + return isRustTextDocument(suspect.document); +} + +function isRustTextDocument(suspect: vscode.TextDocument): suspect is RustTextDocument { + return suspect.languageId === "rust"; +} diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts index 6a7aeb6022..bd6e3ada08 100644 --- a/editors/code/src/rust-analyzer-api.ts +++ b/editors/code/src/rust-analyzer-api.ts @@ -98,8 +98,8 @@ export namespace InlayHint { range: lc.Range; label: string; } - export type TypeHint = Common & { kind: Kind.TypeHint; } - export type ParamHint = Common & { kind: Kind.ParamHint; } + export type TypeHint = Common & { kind: Kind.TypeHint }; + export type ParamHint = Common & { kind: Kind.ParamHint }; } export interface InlayHintsParams { textDocument: lc.TextDocumentIdentifier; From fd709c0c0435f80c593a42e27ca14fcdc561379a Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 29 Feb 2020 21:10:16 +0200 Subject: [PATCH 03/10] vscode: simpify --- editors/code/src/inlay_hints.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index bbe8f67ef5..ac07b2ea03 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -101,14 +101,10 @@ class HintsUpdater { clearHints() { for (const file of this.sourceFiles) { file.inlaysRequest?.cancel(); - this.renderHints(file, []); + file.renderHints([], this.client.protocol2CodeConverter) } } - private renderHints(file: RustSourceFile, hints: ra.InlayHint[]) { - file.renderHints(hints, this.client.protocol2CodeConverter); - } - refreshRustDocument(document: RustTextDocument) { if (!this.enabled) return; From 057cd959dab18133db418979f5c711a1f98c007c Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 29 Feb 2020 21:14:10 +0200 Subject: [PATCH 04/10] vscode: add dat semicolon --- editors/code/src/inlay_hints.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index ac07b2ea03..7b9dcfb227 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -101,7 +101,7 @@ class HintsUpdater { clearHints() { for (const file of this.sourceFiles) { file.inlaysRequest?.cancel(); - file.renderHints([], this.client.protocol2CodeConverter) + file.renderHints([], this.client.protocol2CodeConverter); } } From 3d93e2108e182350e8bbf51d02a76c85ef831f8e Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 29 Feb 2020 23:19:58 +0200 Subject: [PATCH 05/10] vscode: refresh all editors on text changes, simplify inlays api --- editors/code/src/inlay_hints.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index 7b9dcfb227..161b34037b 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -9,9 +9,7 @@ export function activateInlayHints(ctx: Ctx) { const hintsUpdater = new HintsUpdater(ctx.client); vscode.window.onDidChangeVisibleTextEditors( - visibleEditors => hintsUpdater.refreshVisibleRustEditors( - visibleEditors.filter(isRustTextEditor) - ), + () => hintsUpdater.refreshVisibleRustEditors(), null, ctx.subscriptions ); @@ -21,7 +19,7 @@ export function activateInlayHints(ctx: Ctx) { if (contentChanges.length === 0) return; if (!isRustTextDocument(document)) return; - hintsUpdater.refreshRustDocument(document); + hintsUpdater.forceRefreshVisibleRustEditors(); }, null, ctx.subscriptions @@ -92,7 +90,7 @@ class HintsUpdater { this.enabled = enabled; if (this.enabled) { - this.refreshVisibleRustEditors(vscode.window.visibleTextEditors.filter(isRustTextEditor)); + this.refreshVisibleRustEditors(); } else { this.clearHints(); } @@ -105,20 +103,20 @@ class HintsUpdater { } } - refreshRustDocument(document: RustTextDocument) { + forceRefreshVisibleRustEditors() { if (!this.enabled) return; - const file = this.sourceFiles.getSourceFile(document.uri.toString()); - - assert(!!file, "Document must be opened in some text editor!"); - - void file.fetchAndRenderHints(this.client); + for (const file of this.sourceFiles) { + void file.fetchAndRenderHints(this.client); + } } - refreshVisibleRustEditors(visibleEditors: RustTextEditor[]) { + refreshVisibleRustEditors() { if (!this.enabled) return; - const visibleSourceFiles = this.sourceFiles.drainEditors(visibleEditors); + const visibleSourceFiles = this.sourceFiles.drainEditors( + vscode.window.visibleTextEditors.filter(isRustTextEditor) + ); // Cancel requests for source files whose editors were disposed (leftovers after drain). for (const { inlaysRequest } of this.sourceFiles) inlaysRequest?.cancel(); From a63446f2549afbeafe632c425112b7c38b5c9991 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 7 Mar 2020 14:07:44 +0200 Subject: [PATCH 06/10] vscode: prerefactor util.ts and ctx.ts --- editors/code/src/ctx.ts | 12 +++++------- editors/code/src/util.ts | 12 +++++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index b4e983a0cd..25ef38aed0 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -3,7 +3,7 @@ import * as lc from 'vscode-languageclient'; import { Config } from './config'; import { createClient } from './client'; -import { isRustDocument } from './util'; +import { isRustEditor, RustEditor } from './util'; export class Ctx { private constructor( @@ -22,17 +22,15 @@ export class Ctx { return res; } - get activeRustEditor(): vscode.TextEditor | undefined { + get activeRustEditor(): RustEditor | undefined { const editor = vscode.window.activeTextEditor; - return editor && isRustDocument(editor.document) + return editor && isRustEditor(editor) ? editor : undefined; } - get visibleRustEditors(): vscode.TextEditor[] { - return vscode.window.visibleTextEditors.filter( - editor => isRustDocument(editor.document), - ); + get visibleRustEditors(): RustEditor[] { + return vscode.window.visibleTextEditors.filter(isRustEditor); } registerCommand(name: string, factory: (ctx: Ctx) => Cmd) { diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index 7c95769bb8..95a5f1227c 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -1,7 +1,6 @@ import * as lc from "vscode-languageclient"; import * as vscode from "vscode"; import { strict as nativeAssert } from "assert"; -import { TextDocument } from "vscode"; export function assert(condition: boolean, explanation: string): asserts condition { try { @@ -67,9 +66,16 @@ function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -export function isRustDocument(document: TextDocument) { +export type RustDocument = vscode.TextDocument & { languageId: "rust" }; +export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; + +export function isRustDocument(document: vscode.TextDocument): document is RustDocument { return document.languageId === 'rust' // SCM diff views have the same URI as the on-disk document but not the same content && document.uri.scheme !== 'git' && document.uri.scheme !== 'svn'; -} \ No newline at end of file +} + +export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { + return isRustDocument(editor.document); +} From ef52fd543f4048d36e2c37281de4bc343871a62d Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 7 Mar 2020 14:08:08 +0200 Subject: [PATCH 07/10] vscode: remove logic for caching editors as per @matklad --- editors/code/src/inlay_hints.ts | 360 ++++++++++++-------------------- 1 file changed, 137 insertions(+), 223 deletions(-) diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index 161b34037b..6d084362db 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -2,48 +2,32 @@ import * as lc from "vscode-languageclient"; import * as vscode from 'vscode'; import * as ra from './rust-analyzer-api'; -import { Ctx } from './ctx'; -import { sendRequestWithRetry, assert } from './util'; +import { Ctx, Disposable } from './ctx'; +import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, log } from './util'; + export function activateInlayHints(ctx: Ctx) { - const hintsUpdater = new HintsUpdater(ctx.client); - - vscode.window.onDidChangeVisibleTextEditors( - () => hintsUpdater.refreshVisibleRustEditors(), - null, - ctx.subscriptions - ); - - vscode.workspace.onDidChangeTextDocument( - ({ contentChanges, document }) => { - if (contentChanges.length === 0) return; - if (!isRustTextDocument(document)) return; - - hintsUpdater.forceRefreshVisibleRustEditors(); + const maybeUpdater = { + updater: null as null | HintsUpdater, + onConfigChange() { + if (!ctx.config.displayInlayHints) { + return this.dispose(); + } + if (!this.updater) this.updater = HintsUpdater.create(ctx); }, - null, - ctx.subscriptions - ); + dispose() { + this.updater?.dispose(); + this.updater = null; + } + }; + + ctx.pushCleanup(maybeUpdater); vscode.workspace.onDidChangeConfiguration( - async _ => { - // FIXME: ctx.config may have not been refreshed at this point of time, i.e. - // it's on onDidChangeConfiguration() handler may've not executed yet - // (order of invokation is unspecified) - // To fix this we should expose an event emitter from our `Config` itself. - await hintsUpdater.setEnabled(ctx.config.displayInlayHints); - }, - null, - ctx.subscriptions + maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions ); - ctx.pushCleanup({ - dispose() { - hintsUpdater.clearHints(); - } - }); - - hintsUpdater.setEnabled(ctx.config.displayInlayHints); + maybeUpdater.onConfigChange(); } @@ -79,239 +63,169 @@ const paramHints = { } }; -class HintsUpdater { - private sourceFiles = new RustSourceFiles(); - private enabled = false; +class HintsUpdater implements Disposable { + private sourceFiles = new Map(); // map Uri -> RustSourceFile + private readonly disposables: Disposable[] = []; - constructor(readonly client: lc.LanguageClient) { } + private constructor(readonly ctx: Ctx) { } - setEnabled(enabled: boolean) { - if (this.enabled === enabled) return; - this.enabled = enabled; + static create(ctx: Ctx) { + const self = new HintsUpdater(ctx); - if (this.enabled) { - this.refreshVisibleRustEditors(); - } else { - this.clearHints(); - } - } - - clearHints() { - for (const file of this.sourceFiles) { - file.inlaysRequest?.cancel(); - file.renderHints([], this.client.protocol2CodeConverter); - } - } - - forceRefreshVisibleRustEditors() { - if (!this.enabled) return; - - for (const file of this.sourceFiles) { - void file.fetchAndRenderHints(this.client); - } - } - - refreshVisibleRustEditors() { - if (!this.enabled) return; - - const visibleSourceFiles = this.sourceFiles.drainEditors( - vscode.window.visibleTextEditors.filter(isRustTextEditor) + vscode.window.onDidChangeVisibleTextEditors( + self.onDidChangeVisibleTextEditors, + self, + self.disposables ); - // Cancel requests for source files whose editors were disposed (leftovers after drain). - for (const { inlaysRequest } of this.sourceFiles) inlaysRequest?.cancel(); + vscode.workspace.onDidChangeTextDocument( + self.onDidChangeTextDocument, + self, + self.disposables + ); - this.sourceFiles = visibleSourceFiles; - - for (const file of this.sourceFiles) { - if (!file.rerenderHints()) { - void file.fetchAndRenderHints(this.client); + // Set up initial cache shape + ctx.visibleRustEditors.forEach(editor => self.sourceFiles.set( + editor.document.uri.toString(), { + document: editor.document, + inlaysRequest: null, + cachedDecorations: null } - } - } -} + )); + self.syncCacheAndRenderHints(); -/** - * This class encapsulates a map of file uris to respective inlay hints - * request cancellation token source (cts) and an array of editors. - * E.g. - * ``` - * { - * file1.rs -> (cts, (typeDecor, paramDecor), [editor1, editor2]) - * ^-- there is a cts to cancel the in-flight request - * file2.rs -> (cts, null, [editor3]) - * ^-- no decorations are applied to this source file yet - * file3.rs -> (null, (typeDecor, paramDecor), [editor4]) - * } ^-- there is no inflight request - * ``` - * - * Invariants: each stored source file has at least 1 editor. - */ -class RustSourceFiles { - private files = new Map(); - - /** - * Removes `editors` from `this` source files and puts them into a returned - * source files object. cts and decorations are moved to the returned source files. - */ - drainEditors(editors: RustTextEditor[]): RustSourceFiles { - const result = new RustSourceFiles; - - for (const editor of editors) { - const oldFile = this.removeEditor(editor); - const newFile = result.addEditor(editor); - - if (oldFile) newFile.stealCacheFrom(oldFile); - } - - return result; + return self; } - /** - * Remove the editor and if it was the only editor for a source file, - * the source file is removed altogether. - * - * @returns A reference to the source file for this editor or - * null if no such source file was not found. - */ - private removeEditor(editor: RustTextEditor): null | RustSourceFile { - const uri = editor.document.uri.toString(); - - const file = this.files.get(uri); - if (!file) return null; - - const editorIndex = file.editors.findIndex(suspect => areEditorsEqual(suspect, editor)); - - if (editorIndex >= 0) { - file.editors.splice(editorIndex, 1); - - if (file.editors.length === 0) this.files.delete(uri); - } - - return file; + dispose() { + this.sourceFiles.forEach(file => file.inlaysRequest?.cancel()); + this.ctx.visibleRustEditors.forEach(editor => this.renderDecorations(editor, { param: [], type: [] })); + this.disposables.forEach(d => d.dispose()); } - /** - * @returns A reference to an existing source file or newly created one for the editor. - */ - private addEditor(editor: RustTextEditor): RustSourceFile { - const uri = editor.document.uri.toString(); - const file = this.files.get(uri); - - if (!file) { - const newFile = new RustSourceFile([editor]); - this.files.set(uri, newFile); - return newFile; - } - - if (!file.editors.find(suspect => areEditorsEqual(suspect, editor))) { - file.editors.push(editor); - } - return file; + onDidChangeTextDocument({contentChanges, document}: vscode.TextDocumentChangeEvent) { + if (contentChanges.length === 0 || !isRustDocument(document)) return; + log.debug(`[inlays]: changed text doc!`); + this.syncCacheAndRenderHints(); } - getSourceFile(uri: string): undefined | RustSourceFile { - return this.files.get(uri); + private syncCacheAndRenderHints() { + // FIXME: make inlayHints request pass an array of files? + this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => { + if (!hints) return; + + file.cachedDecorations = this.hintsToDecorations(hints); + + for (const editor of this.ctx.visibleRustEditors) { + if (editor.document.uri.toString() === uri) { + this.renderDecorations(editor, file.cachedDecorations); + } + } + })); } - [Symbol.iterator](): IterableIterator { - return this.files.values(); - } -} -class RustSourceFile { - constructor( - /** - * Editors for this source file (one text document may be opened in multiple editors). - * We keep this just an array, because most of the time we have 1 editor for 1 source file. - */ - readonly editors: RustTextEditor[], - /** - * Source of the token to cancel in-flight inlay hints request if any. - */ - public inlaysRequest: null | vscode.CancellationTokenSource = null, + onDidChangeVisibleTextEditors() { + log.debug(`[inlays]: changed visible text editors`); + const newSourceFiles = new Map(); - public decorations: null | { - type: vscode.DecorationOptions[]; - param: vscode.DecorationOptions[]; - } = null - ) { } + // Rerendering all, even up-to-date editors for simplicity + this.ctx.visibleRustEditors.forEach(async editor => { + const uri = editor.document.uri.toString(); + const file = this.sourceFiles.get(uri) ?? { + document: editor.document, + inlaysRequest: null, + cachedDecorations: null + }; + newSourceFiles.set(uri, file); - stealCacheFrom(other: RustSourceFile) { - if (other.inlaysRequest) this.inlaysRequest = other.inlaysRequest; - if (other.decorations) this.decorations = other.decorations; + // No text documents changed, so we may try to use the cache + if (!file.cachedDecorations) { + file.inlaysRequest?.cancel(); - other.inlaysRequest = null; - other.decorations = null; + const hints = await this.fetchHints(file); + if (!hints) return; + + file.cachedDecorations = this.hintsToDecorations(hints); + } + + this.renderDecorations(editor, file.cachedDecorations); + }); + + // Cancel requests for no longer visible (disposed) source files + this.sourceFiles.forEach((file, uri) => { + if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel(); + }); + + this.sourceFiles = newSourceFiles; } - rerenderHints(): boolean { - if (!this.decorations) return false; - - for (const editor of this.editors) { - editor.setDecorations(typeHints.decorationType, this.decorations.type); - editor.setDecorations(paramHints.decorationType, this.decorations.param); - } - return true; + private renderDecorations(editor: RustEditor, decorations: InlaysDecorations) { + editor.setDecorations(typeHints.decorationType, decorations.type); + editor.setDecorations(paramHints.decorationType, decorations.param); } - renderHints(hints: ra.InlayHint[], conv: lc.Protocol2CodeConverter) { - this.decorations = { type: [], param: [] }; + private hintsToDecorations(hints: ra.InlayHint[]): InlaysDecorations { + const decorations: InlaysDecorations = { type: [], param: [] }; + const conv = this.ctx.client.protocol2CodeConverter; for (const hint of hints) { switch (hint.kind) { case ra.InlayHint.Kind.TypeHint: { - this.decorations.type.push(typeHints.toDecoration(hint, conv)); + decorations.type.push(typeHints.toDecoration(hint, conv)); continue; } case ra.InlayHint.Kind.ParamHint: { - this.decorations.param.push(paramHints.toDecoration(hint, conv)); + decorations.param.push(paramHints.toDecoration(hint, conv)); continue; } } } - this.rerenderHints(); + return decorations; } - async fetchAndRenderHints(client: lc.LanguageClient): Promise { - this.inlaysRequest?.cancel(); + lastReqId = 0; + private async fetchHints(file: RustSourceFile): Promise { + const reqId = ++this.lastReqId; + + log.debug(`[inlays]: ${reqId} requesting`); + file.inlaysRequest?.cancel(); const tokenSource = new vscode.CancellationTokenSource(); - this.inlaysRequest = tokenSource; + file.inlaysRequest = tokenSource; - const request = { textDocument: { uri: this.editors[0].document.uri.toString() } }; + const request = { textDocument: { uri: file.document.uri.toString() } }; - try { - const hints = await sendRequestWithRetry(client, ra.inlayHints, request, tokenSource.token); - this.renderHints(hints, client.protocol2CodeConverter); - } catch { - /* ignore */ - } finally { - if (this.inlaysRequest === tokenSource) { - this.inlaysRequest = null; - } - } + return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token) + .catch(_ => { + log.debug(`[inlays]: ${reqId} err`); + return null; + }) + .finally(() => { + if (file.inlaysRequest === tokenSource) { + file.inlaysRequest = null; + log.debug(`[inlays]: ${reqId} got response!`); + } else { + log.debug(`[inlays]: ${reqId} cancelled!`); + } + }) } } -type RustTextDocument = vscode.TextDocument & { languageId: "rust" }; -type RustTextEditor = vscode.TextEditor & { document: RustTextDocument; id: string }; - -function areEditorsEqual(a: RustTextEditor, b: RustTextEditor): boolean { - return a.id === b.id; +interface InlaysDecorations { + type: vscode.DecorationOptions[]; + param: vscode.DecorationOptions[]; } -function isRustTextEditor(suspect: vscode.TextEditor & { id?: unknown }): suspect is RustTextEditor { - // Dirty hack, we need to access private vscode editor id, - // see https://github.com/microsoft/vscode/issues/91788 - assert( - typeof suspect.id === "string", - "Private text editor id is no longer available, please update the workaround!" - ); +interface RustSourceFile { + /* + * Source of the token to cancel in-flight inlay hints request if any. + */ + inlaysRequest: null | vscode.CancellationTokenSource; + /** + * Last applied decorations. + */ + cachedDecorations: null | InlaysDecorations; - return isRustTextDocument(suspect.document); -} - -function isRustTextDocument(suspect: vscode.TextDocument): suspect is RustTextDocument { - return suspect.languageId === "rust"; + document: RustDocument } From 2734ffa20c4c38f57bb6808d6cf87d0a8aaf1ea5 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 7 Mar 2020 14:34:09 +0200 Subject: [PATCH 08/10] vscode: remove logging from inlays, run fix lint issues --- editors/code/src/inlay_hints.ts | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index 6d084362db..d7fabe795a 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import * as ra from './rust-analyzer-api'; import { Ctx, Disposable } from './ctx'; -import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, log } from './util'; +import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor } from './util'; export function activateInlayHints(ctx: Ctx) { @@ -86,7 +86,8 @@ class HintsUpdater implements Disposable { // Set up initial cache shape ctx.visibleRustEditors.forEach(editor => self.sourceFiles.set( - editor.document.uri.toString(), { + editor.document.uri.toString(), + { document: editor.document, inlaysRequest: null, cachedDecorations: null @@ -104,9 +105,8 @@ class HintsUpdater implements Disposable { this.disposables.forEach(d => d.dispose()); } - onDidChangeTextDocument({contentChanges, document}: vscode.TextDocumentChangeEvent) { + onDidChangeTextDocument({ contentChanges, document }: vscode.TextDocumentChangeEvent) { if (contentChanges.length === 0 || !isRustDocument(document)) return; - log.debug(`[inlays]: changed text doc!`); this.syncCacheAndRenderHints(); } @@ -126,7 +126,6 @@ class HintsUpdater implements Disposable { } onDidChangeVisibleTextEditors() { - log.debug(`[inlays]: changed visible text editors`); const newSourceFiles = new Map(); // Rerendering all, even up-to-date editors for simplicity @@ -184,11 +183,7 @@ class HintsUpdater implements Disposable { return decorations; } - lastReqId = 0; private async fetchHints(file: RustSourceFile): Promise { - const reqId = ++this.lastReqId; - - log.debug(`[inlays]: ${reqId} requesting`); file.inlaysRequest?.cancel(); const tokenSource = new vscode.CancellationTokenSource(); @@ -197,18 +192,12 @@ class HintsUpdater implements Disposable { const request = { textDocument: { uri: file.document.uri.toString() } }; return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token) - .catch(_ => { - log.debug(`[inlays]: ${reqId} err`); - return null; - }) + .catch(_ => null) .finally(() => { if (file.inlaysRequest === tokenSource) { file.inlaysRequest = null; - log.debug(`[inlays]: ${reqId} got response!`); - } else { - log.debug(`[inlays]: ${reqId} cancelled!`); } - }) + }); } } @@ -227,5 +216,5 @@ interface RustSourceFile { */ cachedDecorations: null | InlaysDecorations; - document: RustDocument + document: RustDocument; } From 61a4ea25326b794fe64f37fda323fc704a5f96e9 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 7 Mar 2020 14:37:15 +0200 Subject: [PATCH 09/10] vscode: more privacy for HintsUpdater --- editors/code/src/inlay_hints.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index d7fabe795a..fd7c49c890 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -67,7 +67,7 @@ class HintsUpdater implements Disposable { private sourceFiles = new Map(); // map Uri -> RustSourceFile private readonly disposables: Disposable[] = []; - private constructor(readonly ctx: Ctx) { } + private constructor(private readonly ctx: Ctx) { } static create(ctx: Ctx) { const self = new HintsUpdater(ctx); From 65cecff316e9217eb0f58df189a3f05de5d8d51c Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 7 Mar 2020 14:39:42 +0200 Subject: [PATCH 10/10] vscode: post refactor HintsUpdater (simplify create() -> constructor call) --- editors/code/src/inlay_hints.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index fd7c49c890..e1a82e03e8 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -13,7 +13,7 @@ export function activateInlayHints(ctx: Ctx) { if (!ctx.config.displayInlayHints) { return this.dispose(); } - if (!this.updater) this.updater = HintsUpdater.create(ctx); + if (!this.updater) this.updater = new HintsUpdater(ctx); }, dispose() { this.updater?.dispose(); @@ -67,25 +67,21 @@ class HintsUpdater implements Disposable { private sourceFiles = new Map(); // map Uri -> RustSourceFile private readonly disposables: Disposable[] = []; - private constructor(private readonly ctx: Ctx) { } - - static create(ctx: Ctx) { - const self = new HintsUpdater(ctx); - + constructor(private readonly ctx: Ctx) { vscode.window.onDidChangeVisibleTextEditors( - self.onDidChangeVisibleTextEditors, - self, - self.disposables + this.onDidChangeVisibleTextEditors, + this, + this.disposables ); vscode.workspace.onDidChangeTextDocument( - self.onDidChangeTextDocument, - self, - self.disposables + this.onDidChangeTextDocument, + this, + this.disposables ); // Set up initial cache shape - ctx.visibleRustEditors.forEach(editor => self.sourceFiles.set( + ctx.visibleRustEditors.forEach(editor => this.sourceFiles.set( editor.document.uri.toString(), { document: editor.document, @@ -94,9 +90,7 @@ class HintsUpdater implements Disposable { } )); - self.syncCacheAndRenderHints(); - - return self; + this.syncCacheAndRenderHints(); } dispose() {