aboutsummaryrefslogtreecommitdiff
path: root/editors
diff options
context:
space:
mode:
Diffstat (limited to 'editors')
-rw-r--r--editors/code/src/inlay_hints.ts358
1 files changed, 136 insertions, 222 deletions
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts
index 161b34037..6d084362d 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";
2import * as vscode from 'vscode'; 2import * as vscode from 'vscode';
3import * as ra from './rust-analyzer-api'; 3import * as ra from './rust-analyzer-api';
4 4
5import { Ctx } from './ctx'; 5import { Ctx, Disposable } from './ctx';
6import { sendRequestWithRetry, assert } from './util'; 6import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, log } from './util';
7 7
8export function activateInlayHints(ctx: Ctx) {
9 const hintsUpdater = new HintsUpdater(ctx.client);
10
11 vscode.window.onDidChangeVisibleTextEditors(
12 () => hintsUpdater.refreshVisibleRustEditors(),
13 null,
14 ctx.subscriptions
15 );
16
17 vscode.workspace.onDidChangeTextDocument(
18 ({ contentChanges, document }) => {
19 if (contentChanges.length === 0) return;
20 if (!isRustTextDocument(document)) return;
21 8
22 hintsUpdater.forceRefreshVisibleRustEditors(); 9export function activateInlayHints(ctx: Ctx) {
10 const maybeUpdater = {
11 updater: null as null | HintsUpdater,
12 onConfigChange() {
13 if (!ctx.config.displayInlayHints) {
14 return this.dispose();
15 }
16 if (!this.updater) this.updater = HintsUpdater.create(ctx);
23 }, 17 },
24 null, 18 dispose() {
25 ctx.subscriptions 19 this.updater?.dispose();
26 ); 20 this.updater = null;
21 }
22 };
23
24 ctx.pushCleanup(maybeUpdater);
27 25
28 vscode.workspace.onDidChangeConfiguration( 26 vscode.workspace.onDidChangeConfiguration(
29 async _ => { 27 maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions
30 // FIXME: ctx.config may have not been refreshed at this point of time, i.e.
31 // it's on onDidChangeConfiguration() handler may've not executed yet
32 // (order of invokation is unspecified)
33 // To fix this we should expose an event emitter from our `Config` itself.
34 await hintsUpdater.setEnabled(ctx.config.displayInlayHints);
35 },
36 null,
37 ctx.subscriptions
38 ); 28 );
39 29
40 ctx.pushCleanup({ 30 maybeUpdater.onConfigChange();
41 dispose() {
42 hintsUpdater.clearHints();
43 }
44 });
45
46 hintsUpdater.setEnabled(ctx.config.displayInlayHints);
47} 31}
48 32
49 33
@@ -79,239 +63,169 @@ const paramHints = {
79 } 63 }
80}; 64};
81 65
82class HintsUpdater { 66class HintsUpdater implements Disposable {
83 private sourceFiles = new RustSourceFiles(); 67 private sourceFiles = new Map<string, RustSourceFile>(); // map Uri -> RustSourceFile
84 private enabled = false; 68 private readonly disposables: Disposable[] = [];
85
86 constructor(readonly client: lc.LanguageClient) { }
87 69
88 setEnabled(enabled: boolean) { 70 private constructor(readonly ctx: Ctx) { }
89 if (this.enabled === enabled) return;
90 this.enabled = enabled;
91 71
92 if (this.enabled) { 72 static create(ctx: Ctx) {
93 this.refreshVisibleRustEditors(); 73 const self = new HintsUpdater(ctx);
94 } else {
95 this.clearHints();
96 }
97 }
98
99 clearHints() {
100 for (const file of this.sourceFiles) {
101 file.inlaysRequest?.cancel();
102 file.renderHints([], this.client.protocol2CodeConverter);
103 }
104 }
105
106 forceRefreshVisibleRustEditors() {
107 if (!this.enabled) return;
108 74
109 for (const file of this.sourceFiles) { 75 vscode.window.onDidChangeVisibleTextEditors(
110 void file.fetchAndRenderHints(this.client); 76 self.onDidChangeVisibleTextEditors,
111 } 77 self,
112 } 78 self.disposables
113 79 );
114 refreshVisibleRustEditors() {
115 if (!this.enabled) return;
116 80
117 const visibleSourceFiles = this.sourceFiles.drainEditors( 81 vscode.workspace.onDidChangeTextDocument(
118 vscode.window.visibleTextEditors.filter(isRustTextEditor) 82 self.onDidChangeTextDocument,
83 self,
84 self.disposables
119 ); 85 );
120 86
121 // Cancel requests for source files whose editors were disposed (leftovers after drain). 87 // Set up initial cache shape
122 for (const { inlaysRequest } of this.sourceFiles) inlaysRequest?.cancel(); 88 ctx.visibleRustEditors.forEach(editor => self.sourceFiles.set(
89 editor.document.uri.toString(), {
90 document: editor.document,
91 inlaysRequest: null,
92 cachedDecorations: null
93 }
94 ));
123 95
124 this.sourceFiles = visibleSourceFiles; 96 self.syncCacheAndRenderHints();
125 97
126 for (const file of this.sourceFiles) { 98 return self;
127 if (!file.rerenderHints()) {
128 void file.fetchAndRenderHints(this.client);
129 }
130 }
131 } 99 }
132}
133
134 100
135/** 101 dispose() {
136 * This class encapsulates a map of file uris to respective inlay hints 102 this.sourceFiles.forEach(file => file.inlaysRequest?.cancel());
137 * request cancellation token source (cts) and an array of editors. 103 this.ctx.visibleRustEditors.forEach(editor => this.renderDecorations(editor, { param: [], type: [] }));
138 * E.g. 104 this.disposables.forEach(d => d.dispose());
139 * ``` 105 }
140 * {
141 * file1.rs -> (cts, (typeDecor, paramDecor), [editor1, editor2])
142 * ^-- there is a cts to cancel the in-flight request
143 * file2.rs -> (cts, null, [editor3])
144 * ^-- no decorations are applied to this source file yet
145 * file3.rs -> (null, (typeDecor, paramDecor), [editor4])
146 * } ^-- there is no inflight request
147 * ```
148 *
149 * Invariants: each stored source file has at least 1 editor.
150 */
151class RustSourceFiles {
152 private files = new Map<string, RustSourceFile>();
153 106
154 /** 107 onDidChangeTextDocument({contentChanges, document}: vscode.TextDocumentChangeEvent) {
155 * Removes `editors` from `this` source files and puts them into a returned 108 if (contentChanges.length === 0 || !isRustDocument(document)) return;
156 * source files object. cts and decorations are moved to the returned source files. 109 log.debug(`[inlays]: changed text doc!`);
157 */ 110 this.syncCacheAndRenderHints();
158 drainEditors(editors: RustTextEditor[]): RustSourceFiles { 111 }
159 const result = new RustSourceFiles;
160 112
161 for (const editor of editors) { 113 private syncCacheAndRenderHints() {
162 const oldFile = this.removeEditor(editor); 114 // FIXME: make inlayHints request pass an array of files?
163 const newFile = result.addEditor(editor); 115 this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => {
116 if (!hints) return;
164 117
165 if (oldFile) newFile.stealCacheFrom(oldFile); 118 file.cachedDecorations = this.hintsToDecorations(hints);
166 }
167 119
168 return result; 120 for (const editor of this.ctx.visibleRustEditors) {
121 if (editor.document.uri.toString() === uri) {
122 this.renderDecorations(editor, file.cachedDecorations);
123 }
124 }
125 }));
169 } 126 }
170 127
171 /** 128 onDidChangeVisibleTextEditors() {
172 * Remove the editor and if it was the only editor for a source file, 129 log.debug(`[inlays]: changed visible text editors`);
173 * the source file is removed altogether. 130 const newSourceFiles = new Map<string, RustSourceFile>();
174 *
175 * @returns A reference to the source file for this editor or
176 * null if no such source file was not found.
177 */
178 private removeEditor(editor: RustTextEditor): null | RustSourceFile {
179 const uri = editor.document.uri.toString();
180 131
181 const file = this.files.get(uri); 132 // Rerendering all, even up-to-date editors for simplicity
182 if (!file) return null; 133 this.ctx.visibleRustEditors.forEach(async editor => {
134 const uri = editor.document.uri.toString();
135 const file = this.sourceFiles.get(uri) ?? {
136 document: editor.document,
137 inlaysRequest: null,
138 cachedDecorations: null
139 };
140 newSourceFiles.set(uri, file);
183 141
184 const editorIndex = file.editors.findIndex(suspect => areEditorsEqual(suspect, editor)); 142 // No text documents changed, so we may try to use the cache
143 if (!file.cachedDecorations) {
144 file.inlaysRequest?.cancel();
185 145
186 if (editorIndex >= 0) { 146 const hints = await this.fetchHints(file);
187 file.editors.splice(editorIndex, 1); 147 if (!hints) return;
188 148
189 if (file.editors.length === 0) this.files.delete(uri); 149 file.cachedDecorations = this.hintsToDecorations(hints);
190 } 150 }
191
192 return file;
193 }
194
195 /**
196 * @returns A reference to an existing source file or newly created one for the editor.
197 */
198 private addEditor(editor: RustTextEditor): RustSourceFile {
199 const uri = editor.document.uri.toString();
200 const file = this.files.get(uri);
201
202 if (!file) {
203 const newFile = new RustSourceFile([editor]);
204 this.files.set(uri, newFile);
205 return newFile;
206 }
207 151
208 if (!file.editors.find(suspect => areEditorsEqual(suspect, editor))) { 152 this.renderDecorations(editor, file.cachedDecorations);
209 file.editors.push(editor); 153 });
210 }
211 return file;
212 }
213 154
214 getSourceFile(uri: string): undefined | RustSourceFile { 155 // Cancel requests for no longer visible (disposed) source files
215 return this.files.get(uri); 156 this.sourceFiles.forEach((file, uri) => {
216 } 157 if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel();
158 });
217 159
218 [Symbol.iterator](): IterableIterator<RustSourceFile> { 160 this.sourceFiles = newSourceFiles;
219 return this.files.values();
220 }
221}
222class RustSourceFile {
223 constructor(
224 /**
225 * Editors for this source file (one text document may be opened in multiple editors).
226 * We keep this just an array, because most of the time we have 1 editor for 1 source file.
227 */
228 readonly editors: RustTextEditor[],
229 /**
230 * Source of the token to cancel in-flight inlay hints request if any.
231 */
232 public inlaysRequest: null | vscode.CancellationTokenSource = null,
233
234 public decorations: null | {
235 type: vscode.DecorationOptions[];
236 param: vscode.DecorationOptions[];
237 } = null
238 ) { }
239
240 stealCacheFrom(other: RustSourceFile) {
241 if (other.inlaysRequest) this.inlaysRequest = other.inlaysRequest;
242 if (other.decorations) this.decorations = other.decorations;
243
244 other.inlaysRequest = null;
245 other.decorations = null;
246 } 161 }
247 162
248 rerenderHints(): boolean { 163 private renderDecorations(editor: RustEditor, decorations: InlaysDecorations) {
249 if (!this.decorations) return false; 164 editor.setDecorations(typeHints.decorationType, decorations.type);
250 165 editor.setDecorations(paramHints.decorationType, decorations.param);
251 for (const editor of this.editors) {
252 editor.setDecorations(typeHints.decorationType, this.decorations.type);
253 editor.setDecorations(paramHints.decorationType, this.decorations.param);
254 }
255 return true;
256 } 166 }
257 167
258 renderHints(hints: ra.InlayHint[], conv: lc.Protocol2CodeConverter) { 168 private hintsToDecorations(hints: ra.InlayHint[]): InlaysDecorations {
259 this.decorations = { type: [], param: [] }; 169 const decorations: InlaysDecorations = { type: [], param: [] };
170 const conv = this.ctx.client.protocol2CodeConverter;
260 171
261 for (const hint of hints) { 172 for (const hint of hints) {
262 switch (hint.kind) { 173 switch (hint.kind) {
263 case ra.InlayHint.Kind.TypeHint: { 174 case ra.InlayHint.Kind.TypeHint: {
264 this.decorations.type.push(typeHints.toDecoration(hint, conv)); 175 decorations.type.push(typeHints.toDecoration(hint, conv));
265 continue; 176 continue;
266 } 177 }
267 case ra.InlayHint.Kind.ParamHint: { 178 case ra.InlayHint.Kind.ParamHint: {
268 this.decorations.param.push(paramHints.toDecoration(hint, conv)); 179 decorations.param.push(paramHints.toDecoration(hint, conv));
269 continue; 180 continue;
270 } 181 }
271 } 182 }
272 } 183 }
273 this.rerenderHints(); 184 return decorations;
274 } 185 }
275 186
276 async fetchAndRenderHints(client: lc.LanguageClient): Promise<void> { 187 lastReqId = 0;
277 this.inlaysRequest?.cancel(); 188 private async fetchHints(file: RustSourceFile): Promise<null | ra.InlayHint[]> {
189 const reqId = ++this.lastReqId;
190
191 log.debug(`[inlays]: ${reqId} requesting`);
192 file.inlaysRequest?.cancel();
278 193
279 const tokenSource = new vscode.CancellationTokenSource(); 194 const tokenSource = new vscode.CancellationTokenSource();
280 this.inlaysRequest = tokenSource; 195 file.inlaysRequest = tokenSource;
281 196
282 const request = { textDocument: { uri: this.editors[0].document.uri.toString() } }; 197 const request = { textDocument: { uri: file.document.uri.toString() } };
283 198
284 try { 199 return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token)
285 const hints = await sendRequestWithRetry(client, ra.inlayHints, request, tokenSource.token); 200 .catch(_ => {
286 this.renderHints(hints, client.protocol2CodeConverter); 201 log.debug(`[inlays]: ${reqId} err`);
287 } catch { 202 return null;
288 /* ignore */ 203 })
289 } finally { 204 .finally(() => {
290 if (this.inlaysRequest === tokenSource) { 205 if (file.inlaysRequest === tokenSource) {
291 this.inlaysRequest = null; 206 file.inlaysRequest = null;
292 } 207 log.debug(`[inlays]: ${reqId} got response!`);
293 } 208 } else {
209 log.debug(`[inlays]: ${reqId} cancelled!`);
210 }
211 })
294 } 212 }
295} 213}
296 214
297type RustTextDocument = vscode.TextDocument & { languageId: "rust" }; 215interface InlaysDecorations {
298type RustTextEditor = vscode.TextEditor & { document: RustTextDocument; id: string }; 216 type: vscode.DecorationOptions[];
299 217 param: vscode.DecorationOptions[];
300function areEditorsEqual(a: RustTextEditor, b: RustTextEditor): boolean {
301 return a.id === b.id;
302} 218}
303 219
304function isRustTextEditor(suspect: vscode.TextEditor & { id?: unknown }): suspect is RustTextEditor { 220interface RustSourceFile {
305 // Dirty hack, we need to access private vscode editor id, 221 /*
306 // see https://github.com/microsoft/vscode/issues/91788 222 * Source of the token to cancel in-flight inlay hints request if any.
307 assert( 223 */
308 typeof suspect.id === "string", 224 inlaysRequest: null | vscode.CancellationTokenSource;
309 "Private text editor id is no longer available, please update the workaround!" 225 /**
310 ); 226 * Last applied decorations.
311 227 */
312 return isRustTextDocument(suspect.document); 228 cachedDecorations: null | InlaysDecorations;
313}
314 229
315function isRustTextDocument(suspect: vscode.TextDocument): suspect is RustTextDocument { 230 document: RustDocument
316 return suspect.languageId === "rust";
317} 231}