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