1 import * as lc from "vscode-languageclient";
2 import * as vscode from 'vscode';
3 import * as ra from './lsp_ext';
5 import { Ctx, Disposable } from './ctx';
6 import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, sleep } from './util';
8 interface InlayHintStyle {
9 decorationType: vscode.TextEditorDecorationType;
10 toDecoration(hint: ra.InlayHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions;
13 interface InlayHintsStyles {
14 typeHints: InlayHintStyle;
15 paramHints: InlayHintStyle;
16 chainingHints: InlayHintStyle;
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;
29 if (!enabled) return this.dispose();
33 this.updater.updateInlayHintsStyles();
34 this.updater.syncCacheAndRenderHints();
36 this.updater = new HintsUpdater(ctx);
40 this.updater?.dispose();
45 ctx.pushCleanup(maybeUpdater);
47 vscode.workspace.onDidChangeConfiguration(
48 maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions
51 maybeUpdater.onConfigChange().catch(console.error);
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];
63 const fg = new vscode.ThemeColor(`rust_analyzer.inlayHints.foreground.${hintKind}Hints`);
64 const bg = new vscode.ThemeColor(`rust_analyzer.inlayHints.background.${hintKind}Hints`);
66 decorationType: vscode.window.createTextEditorDecorationType({
72 textDecoration: smallerHints ? ";font-size:smaller" : "none",
75 toDecoration(hint: ra.InlayHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
77 range: conv.asRange(hint.range),
78 renderOptions: { [pos]: { contentText: render(hint.label) } }
84 const smallHintsStyles = {
85 typeHints: createHintStyle("type", true),
86 paramHints: createHintStyle("parameter", true),
87 chainingHints: createHintStyle("chaining", true),
90 const biggerHintsStyles = {
91 typeHints: createHintStyle("type", false),
92 paramHints: createHintStyle("parameter", false),
93 chainingHints: createHintStyle("chaining", false),
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;
102 constructor(private readonly ctx: Ctx) {
103 vscode.window.onDidChangeVisibleTextEditors(
104 this.onDidChangeVisibleTextEditors,
109 vscode.workspace.onDidChangeTextDocument(
110 this.onDidChangeTextDocument,
115 // Set up initial cache shape
116 ctx.visibleRustEditors.forEach(editor => this.sourceFiles.set(
117 editor.document.uri.toString(),
119 document: editor.document,
121 cachedDecorations: null
125 this.updateInlayHintsStyles();
126 this.syncCacheAndRenderHints();
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());
135 onDidChangeTextDocument({ contentChanges, document }: vscode.TextDocumentChangeEvent) {
136 if (contentChanges.length === 0 || !isRustDocument(document)) return;
137 this.syncCacheAndRenderHints();
140 updateInlayHintsStyles() {
141 const inlayHintsStyles = this.ctx.config.inlayHints.smallerHints ? smallHintsStyles : biggerHintsStyles;
143 if (inlayHintsStyles !== this.inlayHintsStyles) {
144 this.pendingDisposeDecorations = this.inlayHintsStyles;
145 this.inlayHintsStyles = inlayHintsStyles;
149 syncCacheAndRenderHints() {
150 this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => {
153 file.cachedDecorations = this.hintsToDecorations(hints);
155 for (const editor of this.ctx.visibleRustEditors) {
156 if (editor.document.uri.toString() === uri) {
157 this.renderDecorations(editor, file.cachedDecorations);
163 onDidChangeVisibleTextEditors() {
164 const newSourceFiles = new Map<string, RustSourceFile>();
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,
172 cachedDecorations: null
174 newSourceFiles.set(uri, file);
176 // No text documents changed, so we may try to use the cache
177 if (!file.cachedDecorations) {
178 const hints = await this.fetchHints(file);
181 file.cachedDecorations = this.hintsToDecorations(hints);
184 this.renderDecorations(editor, file.cachedDecorations);
187 // Cancel requests for no longer visible (disposed) source files
188 this.sourceFiles.forEach((file, uri) => {
189 if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel();
192 this.sourceFiles = newSourceFiles;
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, []);
203 editor.setDecorations(typeHints.decorationType, decorations.type);
204 editor.setDecorations(paramHints.decorationType, decorations.param);
205 editor.setDecorations(chainingHints.decorationType, decorations.chaining);
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;
213 for (const hint of hints) {
215 case ra.InlayHint.Kind.TypeHint: {
216 decorations.type.push(typeHints.toDecoration(hint, conv));
219 case ra.InlayHint.Kind.ParamHint: {
220 decorations.param.push(paramHints.toDecoration(hint, conv));
223 case ra.InlayHint.Kind.ChainingHint: {
224 decorations.chaining.push(chainingHints.toDecoration(hint, conv));
232 private async fetchHints(file: RustSourceFile): Promise<null | ra.InlayHint[]> {
233 file.inlaysRequest?.cancel();
235 const tokenSource = new vscode.CancellationTokenSource();
236 file.inlaysRequest = tokenSource;
238 const request = { textDocument: { uri: file.document.uri.toString() } };
240 return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token)
243 if (file.inlaysRequest === tokenSource) {
244 file.inlaysRequest = null;
250 interface InlaysDecorations {
251 type: vscode.DecorationOptions[];
252 param: vscode.DecorationOptions[];
253 chaining: vscode.DecorationOptions[];
256 interface RustSourceFile {
258 * Source of the token to cancel in-flight inlay hints request if any.
260 inlaysRequest: null | vscode.CancellationTokenSource;
262 * Last applied decorations.
264 cachedDecorations: null | InlaysDecorations;
266 document: RustDocument;