Skip to content

Commit aff82cf

Browse files
bors[bot]Veetaha
andauthored
Merge #3378
3378: vscode: redesign inlay hints to be capable of handling multiple editors for one source file r=Veetaha a=Veetaha Fixes: #3008 (inlay corruption with multiple editors for one file). Fixes: #3319 (unnecessary requests for inlay hints when switching unrelated source files or output/log/console windows) Also, I don't know how, but the problem described in #3057 doesn't appear for me anymore (maybe it was some fix on the server-side, idk), the inlay hints are displaying right away. Though the last time I checked this it was caused by the server returning an empty array of hints and responding with a very big latency, I am not sure that this redesign actually fixed #3057.... We didn't handle the case when one rust source file is open in multiple editors in vscode (e.g. by manually adding another editor for the same file or by opening an inline git diff view or just any kind of preview within the same file). The git diff preview is actually quite special because it causes memory leaks in vscode (microsoft/vscode#91782). It is not removed from `visibleEditors` once it is closed. However, this bug doesn't affect the inlay hints anymore, because we don't issue a request and set inlay hints for each editor in isolation. Editors are grouped by their respective files and we issue requests only for files and then update all duplicate editors using the results (so we just update the decorations for already closed git diff preview read-only editors). Also, note on a hack I had to use. `vscode.TextEdtior` identity is not actually public, its `id` field is not exposed to us. I created a dedicated upstream issue for this (microsoft/vscode#91788). Regarding #3319: the newly designed hints client doesn't issue requests for type hints when switching the visible editors if it has them already cached (though it does rerender things anyway, but this could be optimized in future if so wanted). <details> <summary>Before</summary> ![bug_demo](https://user-images.githubusercontent.com/36276403/75613171-3cd0d480-5b33-11ea-9066-954fb2fb18a5.gif) </details> <details> <summary> After </summary> ![multi-cursor-replace](https://user-images.githubusercontent.com/36276403/75612710-d5b12100-5b2e-11ea-99ba-214b4219e6d3.gif) </details> Co-authored-by: Veetaha <gerzoh1@gmail.com>
2 parents 013e908 + 65cecff commit aff82cf

File tree

4 files changed

+202
-134
lines changed

4 files changed

+202
-134
lines changed

editors/code/src/ctx.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as lc from 'vscode-languageclient';
33

44
import { Config } from './config';
55
import { createClient } from './client';
6-
import { isRustDocument } from './util';
6+
import { isRustEditor, RustEditor } from './util';
77

88
export class Ctx {
99
private constructor(
@@ -22,17 +22,15 @@ export class Ctx {
2222
return res;
2323
}
2424

25-
get activeRustEditor(): vscode.TextEditor | undefined {
25+
get activeRustEditor(): RustEditor | undefined {
2626
const editor = vscode.window.activeTextEditor;
27-
return editor && isRustDocument(editor.document)
27+
return editor && isRustEditor(editor)
2828
? editor
2929
: undefined;
3030
}
3131

32-
get visibleRustEditors(): vscode.TextEditor[] {
33-
return vscode.window.visibleTextEditors.filter(
34-
editor => isRustDocument(editor.document),
35-
);
32+
get visibleRustEditors(): RustEditor[] {
33+
return vscode.window.visibleTextEditors.filter(isRustEditor);
3634
}
3735

3836
registerCommand(name: string, factory: (ctx: Ctx) => Cmd) {

editors/code/src/inlay_hints.ts

Lines changed: 174 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,156 +1,214 @@
1+
import * as lc from "vscode-languageclient";
12
import * as vscode from 'vscode';
23
import * as ra from './rust-analyzer-api';
34

4-
import { Ctx } from './ctx';
5-
import { log, sendRequestWithRetry, isRustDocument } from './util';
5+
import { Ctx, Disposable } from './ctx';
6+
import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor } from './util';
67

7-
export function activateInlayHints(ctx: Ctx) {
8-
const hintsUpdater = new HintsUpdater(ctx);
9-
vscode.window.onDidChangeVisibleTextEditors(
10-
async _ => hintsUpdater.refresh(),
11-
null,
12-
ctx.subscriptions
13-
);
148

15-
vscode.workspace.onDidChangeTextDocument(
16-
async event => {
17-
if (event.contentChanges.length === 0) return;
18-
if (!isRustDocument(event.document)) return;
19-
await hintsUpdater.refresh();
9+
export 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 = new HintsUpdater(ctx);
2017
},
21-
null,
22-
ctx.subscriptions
23-
);
18+
dispose() {
19+
this.updater?.dispose();
20+
this.updater = null;
21+
}
22+
};
23+
24+
ctx.pushCleanup(maybeUpdater);
2425

2526
vscode.workspace.onDidChangeConfiguration(
26-
async _ => hintsUpdater.setEnabled(ctx.config.displayInlayHints),
27-
null,
28-
ctx.subscriptions
27+
maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions
2928
);
3029

31-
ctx.pushCleanup({
32-
dispose() {
33-
hintsUpdater.clear();
30+
maybeUpdater.onConfigChange();
31+
}
32+
33+
34+
const typeHints = {
35+
decorationType: vscode.window.createTextEditorDecorationType({
36+
after: {
37+
color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
38+
fontStyle: "normal",
3439
}
35-
});
40+
}),
3641

37-
// XXX: we don't await this, thus Promise rejections won't be handled, but
38-
// this should never throw in fact...
39-
void hintsUpdater.setEnabled(ctx.config.displayInlayHints);
40-
}
42+
toDecoration(hint: ra.InlayHint.TypeHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
43+
return {
44+
range: conv.asRange(hint.range),
45+
renderOptions: { after: { contentText: `: ${hint.label}` } }
46+
};
47+
}
48+
};
49+
50+
const paramHints = {
51+
decorationType: vscode.window.createTextEditorDecorationType({
52+
before: {
53+
color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
54+
fontStyle: "normal",
55+
}
56+
}),
4157

42-
const typeHintDecorationType = vscode.window.createTextEditorDecorationType({
43-
after: {
44-
color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
45-
fontStyle: "normal",
46-
},
47-
});
48-
49-
const parameterHintDecorationType = vscode.window.createTextEditorDecorationType({
50-
before: {
51-
color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
52-
fontStyle: "normal",
53-
},
54-
});
55-
56-
class HintsUpdater {
57-
private pending = new Map<string, vscode.CancellationTokenSource>();
58-
private ctx: Ctx;
59-
private enabled: boolean;
60-
61-
constructor(ctx: Ctx) {
62-
this.ctx = ctx;
63-
this.enabled = false;
58+
toDecoration(hint: ra.InlayHint.ParamHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
59+
return {
60+
range: conv.asRange(hint.range),
61+
renderOptions: { before: { contentText: `${hint.label}: ` } }
62+
};
6463
}
64+
};
6565

66-
async setEnabled(enabled: boolean): Promise<void> {
67-
log.debug({ enabled, prev: this.enabled });
66+
class HintsUpdater implements Disposable {
67+
private sourceFiles = new Map<string, RustSourceFile>(); // map Uri -> RustSourceFile
68+
private readonly disposables: Disposable[] = [];
6869

69-
if (this.enabled === enabled) return;
70-
this.enabled = enabled;
70+
constructor(private readonly ctx: Ctx) {
71+
vscode.window.onDidChangeVisibleTextEditors(
72+
this.onDidChangeVisibleTextEditors,
73+
this,
74+
this.disposables
75+
);
7176

72-
if (this.enabled) {
73-
return await this.refresh();
74-
} else {
75-
return this.clear();
76-
}
77+
vscode.workspace.onDidChangeTextDocument(
78+
this.onDidChangeTextDocument,
79+
this,
80+
this.disposables
81+
);
82+
83+
// Set up initial cache shape
84+
ctx.visibleRustEditors.forEach(editor => this.sourceFiles.set(
85+
editor.document.uri.toString(),
86+
{
87+
document: editor.document,
88+
inlaysRequest: null,
89+
cachedDecorations: null
90+
}
91+
));
92+
93+
this.syncCacheAndRenderHints();
7794
}
7895

79-
clear() {
80-
this.ctx.visibleRustEditors.forEach(it => {
81-
this.setTypeDecorations(it, []);
82-
this.setParameterDecorations(it, []);
83-
});
96+
dispose() {
97+
this.sourceFiles.forEach(file => file.inlaysRequest?.cancel());
98+
this.ctx.visibleRustEditors.forEach(editor => this.renderDecorations(editor, { param: [], type: [] }));
99+
this.disposables.forEach(d => d.dispose());
100+
}
101+
102+
onDidChangeTextDocument({ contentChanges, document }: vscode.TextDocumentChangeEvent) {
103+
if (contentChanges.length === 0 || !isRustDocument(document)) return;
104+
this.syncCacheAndRenderHints();
84105
}
85106

86-
async refresh() {
87-
if (!this.enabled) return;
88-
await Promise.all(this.ctx.visibleRustEditors.map(it => this.refreshEditor(it)));
107+
private syncCacheAndRenderHints() {
108+
// FIXME: make inlayHints request pass an array of files?
109+
this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => {
110+
if (!hints) return;
111+
112+
file.cachedDecorations = this.hintsToDecorations(hints);
113+
114+
for (const editor of this.ctx.visibleRustEditors) {
115+
if (editor.document.uri.toString() === uri) {
116+
this.renderDecorations(editor, file.cachedDecorations);
117+
}
118+
}
119+
}));
89120
}
90121

91-
private async refreshEditor(editor: vscode.TextEditor): Promise<void> {
92-
const newHints = await this.queryHints(editor.document.uri.toString());
93-
if (newHints == null) return;
94-
95-
const newTypeDecorations = newHints
96-
.filter(hint => hint.kind === ra.InlayKind.TypeHint)
97-
.map(hint => ({
98-
range: this.ctx.client.protocol2CodeConverter.asRange(hint.range),
99-
renderOptions: {
100-
after: {
101-
contentText: `: ${hint.label}`,
102-
},
103-
},
104-
}));
105-
this.setTypeDecorations(editor, newTypeDecorations);
106-
107-
const newParameterDecorations = newHints
108-
.filter(hint => hint.kind === ra.InlayKind.ParameterHint)
109-
.map(hint => ({
110-
range: this.ctx.client.protocol2CodeConverter.asRange(hint.range),
111-
renderOptions: {
112-
before: {
113-
contentText: `${hint.label}: `,
114-
},
115-
},
116-
}));
117-
this.setParameterDecorations(editor, newParameterDecorations);
122+
onDidChangeVisibleTextEditors() {
123+
const newSourceFiles = new Map<string, RustSourceFile>();
124+
125+
// Rerendering all, even up-to-date editors for simplicity
126+
this.ctx.visibleRustEditors.forEach(async editor => {
127+
const uri = editor.document.uri.toString();
128+
const file = this.sourceFiles.get(uri) ?? {
129+
document: editor.document,
130+
inlaysRequest: null,
131+
cachedDecorations: null
132+
};
133+
newSourceFiles.set(uri, file);
134+
135+
// No text documents changed, so we may try to use the cache
136+
if (!file.cachedDecorations) {
137+
file.inlaysRequest?.cancel();
138+
139+
const hints = await this.fetchHints(file);
140+
if (!hints) return;
141+
142+
file.cachedDecorations = this.hintsToDecorations(hints);
143+
}
144+
145+
this.renderDecorations(editor, file.cachedDecorations);
146+
});
147+
148+
// Cancel requests for no longer visible (disposed) source files
149+
this.sourceFiles.forEach((file, uri) => {
150+
if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel();
151+
});
152+
153+
this.sourceFiles = newSourceFiles;
118154
}
119155

120-
private setTypeDecorations(
121-
editor: vscode.TextEditor,
122-
decorations: vscode.DecorationOptions[],
123-
) {
124-
editor.setDecorations(
125-
typeHintDecorationType,
126-
this.enabled ? decorations : [],
127-
);
156+
private renderDecorations(editor: RustEditor, decorations: InlaysDecorations) {
157+
editor.setDecorations(typeHints.decorationType, decorations.type);
158+
editor.setDecorations(paramHints.decorationType, decorations.param);
128159
}
129160

130-
private setParameterDecorations(
131-
editor: vscode.TextEditor,
132-
decorations: vscode.DecorationOptions[],
133-
) {
134-
editor.setDecorations(
135-
parameterHintDecorationType,
136-
this.enabled ? decorations : [],
137-
);
161+
private hintsToDecorations(hints: ra.InlayHint[]): InlaysDecorations {
162+
const decorations: InlaysDecorations = { type: [], param: [] };
163+
const conv = this.ctx.client.protocol2CodeConverter;
164+
165+
for (const hint of hints) {
166+
switch (hint.kind) {
167+
case ra.InlayHint.Kind.TypeHint: {
168+
decorations.type.push(typeHints.toDecoration(hint, conv));
169+
continue;
170+
}
171+
case ra.InlayHint.Kind.ParamHint: {
172+
decorations.param.push(paramHints.toDecoration(hint, conv));
173+
continue;
174+
}
175+
}
176+
}
177+
return decorations;
138178
}
139179

140-
private async queryHints(documentUri: string): Promise<ra.InlayHint[] | null> {
141-
this.pending.get(documentUri)?.cancel();
180+
private async fetchHints(file: RustSourceFile): Promise<null | ra.InlayHint[]> {
181+
file.inlaysRequest?.cancel();
142182

143183
const tokenSource = new vscode.CancellationTokenSource();
144-
this.pending.set(documentUri, tokenSource);
184+
file.inlaysRequest = tokenSource;
145185

146-
const request = { textDocument: { uri: documentUri } };
186+
const request = { textDocument: { uri: file.document.uri.toString() } };
147187

148188
return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token)
149189
.catch(_ => null)
150190
.finally(() => {
151-
if (!tokenSource.token.isCancellationRequested) {
152-
this.pending.delete(documentUri);
191+
if (file.inlaysRequest === tokenSource) {
192+
file.inlaysRequest = null;
153193
}
154194
});
155195
}
156196
}
197+
198+
interface InlaysDecorations {
199+
type: vscode.DecorationOptions[];
200+
param: vscode.DecorationOptions[];
201+
}
202+
203+
interface RustSourceFile {
204+
/*
205+
* Source of the token to cancel in-flight inlay hints request if any.
206+
*/
207+
inlaysRequest: null | vscode.CancellationTokenSource;
208+
/**
209+
* Last applied decorations.
210+
*/
211+
cachedDecorations: null | InlaysDecorations;
212+
213+
document: RustDocument;
214+
}

editors/code/src/rust-analyzer-api.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,20 @@ export interface Runnable {
8686
export const runnables = request<RunnablesParams, Vec<Runnable>>("runnables");
8787

8888

89-
export const enum InlayKind {
90-
TypeHint = "TypeHint",
91-
ParameterHint = "ParameterHint",
92-
}
93-
export interface InlayHint {
94-
range: lc.Range;
95-
kind: InlayKind;
96-
label: string;
89+
90+
export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint;
91+
92+
export namespace InlayHint {
93+
export const enum Kind {
94+
TypeHint = "TypeHint",
95+
ParamHint = "ParameterHint",
96+
}
97+
interface Common {
98+
range: lc.Range;
99+
label: string;
100+
}
101+
export type TypeHint = Common & { kind: Kind.TypeHint };
102+
export type ParamHint = Common & { kind: Kind.ParamHint };
97103
}
98104
export interface InlayHintsParams {
99105
textDocument: lc.TextDocumentIdentifier;

0 commit comments

Comments
 (0)