]> git.lizzy.rs Git - rust.git/blob - editors/code/src/inlay_hints.ts
Drop extensionUri copy
[rust.git] / editors / code / src / inlay_hints.ts
1 import * as lc from "vscode-languageclient";
2 import * as vscode from 'vscode';
3 import * as ra from './lsp_ext';
4
5 import { Ctx, Disposable } from './ctx';
6 import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, sleep } from './util';
7
8 interface InlayHintStyle {
9     decorationType: vscode.TextEditorDecorationType;
10     toDecoration(hint: ra.InlayHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions;
11 };
12
13 interface InlayHintsStyles {
14     typeHints: InlayHintStyle;
15     paramHints: InlayHintStyle;
16     chainingHints: InlayHintStyle;
17 }
18
19
20 export function activateInlayHints(ctx: Ctx) {
21     const maybeUpdater = {
22         updater: null as null | HintsUpdater,
23         async onConfigChange() {
24             const anyEnabled = ctx.config.inlayHints.typeHints
25                 || ctx.config.inlayHints.parameterHints
26                 || ctx.config.inlayHints.chainingHints;
27             const enabled = ctx.config.inlayHints.enable && anyEnabled;
28
29             if (!enabled) return this.dispose();
30
31             await sleep(100);
32             if (this.updater) {
33                 this.updater.updateInlayHintsStyles();
34                 this.updater.syncCacheAndRenderHints();
35             } else {
36                 this.updater = new HintsUpdater(ctx);
37             }
38         },
39         dispose() {
40             this.updater?.dispose();
41             this.updater = null;
42         }
43     };
44
45     ctx.pushCleanup(maybeUpdater);
46
47     vscode.workspace.onDidChangeConfiguration(
48         maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions
49     );
50
51     maybeUpdater.onConfigChange().catch(console.error);
52 }
53
54 function createHintStyle(hintKind: "type" | "parameter" | "chaining", smallerHints: boolean): InlayHintStyle {
55     // U+200C is a zero-width non-joiner to prevent the editor from forming a ligature
56     // between code and type hints
57     const [pos, render] = ({
58         type: ["after", (label: string) => `\u{200c}: ${label}`],
59         parameter: ["before", (label: string) => `${label}: `],
60         chaining: ["after", (label: string) => `\u{200c}: ${label}`],
61     } as const)[hintKind];
62
63     const fg = new vscode.ThemeColor(`rust_analyzer.inlayHints.foreground.${hintKind}Hints`);
64     const bg = new vscode.ThemeColor(`rust_analyzer.inlayHints.background.${hintKind}Hints`);
65     return {
66         decorationType: vscode.window.createTextEditorDecorationType({
67             [pos]: {
68                 color: fg,
69                 backgroundColor: bg,
70                 fontStyle: "normal",
71                 fontWeight: "normal",
72                 textDecoration: smallerHints ? ";font-size:smaller" : "none",
73             },
74         }),
75         toDecoration(hint: ra.InlayHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
76             return {
77                 range: conv.asRange(hint.range),
78                 renderOptions: { [pos]: { contentText: render(hint.label) } }
79             };
80         }
81     };
82 }
83
84 const smallHintsStyles = {
85     typeHints: createHintStyle("type", true),
86     paramHints: createHintStyle("parameter", true),
87     chainingHints: createHintStyle("chaining", true),
88 };
89
90 const biggerHintsStyles = {
91     typeHints: createHintStyle("type", false),
92     paramHints: createHintStyle("parameter", false),
93     chainingHints: createHintStyle("chaining", false),
94 };
95
96 class HintsUpdater implements Disposable {
97     private sourceFiles = new Map<string, RustSourceFile>(); // map Uri -> RustSourceFile
98     private readonly disposables: Disposable[] = [];
99     private pendingDisposeDecorations: undefined | InlayHintsStyles = undefined;
100     private inlayHintsStyles!: InlayHintsStyles;
101
102     constructor(private readonly ctx: Ctx) {
103         vscode.window.onDidChangeVisibleTextEditors(
104             this.onDidChangeVisibleTextEditors,
105             this,
106             this.disposables
107         );
108
109         vscode.workspace.onDidChangeTextDocument(
110             this.onDidChangeTextDocument,
111             this,
112             this.disposables
113         );
114
115         // Set up initial cache shape
116         ctx.visibleRustEditors.forEach(editor => this.sourceFiles.set(
117             editor.document.uri.toString(),
118             {
119                 document: editor.document,
120                 inlaysRequest: null,
121                 cachedDecorations: null
122             }
123         ));
124
125         this.updateInlayHintsStyles();
126         this.syncCacheAndRenderHints();
127     }
128
129     dispose() {
130         this.sourceFiles.forEach(file => file.inlaysRequest?.cancel());
131         this.ctx.visibleRustEditors.forEach(editor => this.renderDecorations(editor, { param: [], type: [], chaining: [] }));
132         this.disposables.forEach(d => d.dispose());
133     }
134
135     onDidChangeTextDocument({ contentChanges, document }: vscode.TextDocumentChangeEvent) {
136         if (contentChanges.length === 0 || !isRustDocument(document)) return;
137         this.syncCacheAndRenderHints();
138     }
139
140     updateInlayHintsStyles() {
141         const inlayHintsStyles = this.ctx.config.inlayHints.smallerHints ? smallHintsStyles : biggerHintsStyles;
142
143         if (inlayHintsStyles !== this.inlayHintsStyles) {
144             this.pendingDisposeDecorations = this.inlayHintsStyles;
145             this.inlayHintsStyles = inlayHintsStyles;
146         }
147     }
148
149     syncCacheAndRenderHints() {
150         this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => {
151             if (!hints) return;
152
153             file.cachedDecorations = this.hintsToDecorations(hints);
154
155             for (const editor of this.ctx.visibleRustEditors) {
156                 if (editor.document.uri.toString() === uri) {
157                     this.renderDecorations(editor, file.cachedDecorations);
158                 }
159             }
160         }));
161     }
162
163     onDidChangeVisibleTextEditors() {
164         const newSourceFiles = new Map<string, RustSourceFile>();
165
166         // Rerendering all, even up-to-date editors for simplicity
167         this.ctx.visibleRustEditors.forEach(async editor => {
168             const uri = editor.document.uri.toString();
169             const file = this.sourceFiles.get(uri) ?? {
170                 document: editor.document,
171                 inlaysRequest: null,
172                 cachedDecorations: null
173             };
174             newSourceFiles.set(uri, file);
175
176             // No text documents changed, so we may try to use the cache
177             if (!file.cachedDecorations) {
178                 const hints = await this.fetchHints(file);
179                 if (!hints) return;
180
181                 file.cachedDecorations = this.hintsToDecorations(hints);
182             }
183
184             this.renderDecorations(editor, file.cachedDecorations);
185         });
186
187         // Cancel requests for no longer visible (disposed) source files
188         this.sourceFiles.forEach((file, uri) => {
189             if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel();
190         });
191
192         this.sourceFiles = newSourceFiles;
193     }
194
195     private renderDecorations(editor: RustEditor, decorations: InlaysDecorations) {
196         const { typeHints, paramHints, chainingHints } = this.inlayHintsStyles;
197         if (this.pendingDisposeDecorations !== undefined) {
198             const { typeHints, paramHints, chainingHints } = this.pendingDisposeDecorations;
199             editor.setDecorations(typeHints.decorationType, []);
200             editor.setDecorations(paramHints.decorationType, []);
201             editor.setDecorations(chainingHints.decorationType, []);
202         }
203         editor.setDecorations(typeHints.decorationType, decorations.type);
204         editor.setDecorations(paramHints.decorationType, decorations.param);
205         editor.setDecorations(chainingHints.decorationType, decorations.chaining);
206     }
207
208     private hintsToDecorations(hints: ra.InlayHint[]): InlaysDecorations {
209         const { typeHints, paramHints, chainingHints } = this.inlayHintsStyles;
210         const decorations: InlaysDecorations = { type: [], param: [], chaining: [] };
211         const conv = this.ctx.client.protocol2CodeConverter;
212
213         for (const hint of hints) {
214             switch (hint.kind) {
215                 case ra.InlayHint.Kind.TypeHint: {
216                     decorations.type.push(typeHints.toDecoration(hint, conv));
217                     continue;
218                 }
219                 case ra.InlayHint.Kind.ParamHint: {
220                     decorations.param.push(paramHints.toDecoration(hint, conv));
221                     continue;
222                 }
223                 case ra.InlayHint.Kind.ChainingHint: {
224                     decorations.chaining.push(chainingHints.toDecoration(hint, conv));
225                     continue;
226                 }
227             }
228         }
229         return decorations;
230     }
231
232     private async fetchHints(file: RustSourceFile): Promise<null | ra.InlayHint[]> {
233         file.inlaysRequest?.cancel();
234
235         const tokenSource = new vscode.CancellationTokenSource();
236         file.inlaysRequest = tokenSource;
237
238         const request = { textDocument: { uri: file.document.uri.toString() } };
239
240         return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token)
241             .catch(_ => null)
242             .finally(() => {
243                 if (file.inlaysRequest === tokenSource) {
244                     file.inlaysRequest = null;
245                 }
246             });
247     }
248 }
249
250 interface InlaysDecorations {
251     type: vscode.DecorationOptions[];
252     param: vscode.DecorationOptions[];
253     chaining: vscode.DecorationOptions[];
254 }
255
256 interface RustSourceFile {
257     /**
258      * Source of the token to cancel in-flight inlay hints request if any.
259      */
260     inlaysRequest: null | vscode.CancellationTokenSource;
261     /**
262      * Last applied decorations.
263      */
264     cachedDecorations: null | InlaysDecorations;
265
266     document: RustDocument;
267 }