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';
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;
18 if (!enabled) return this.dispose();
22 this.updater.syncCacheAndRenderHints();
24 this.updater = new HintsUpdater(ctx);
28 this.updater?.dispose();
33 ctx.pushCleanup(maybeUpdater);
35 vscode.workspace.onDidChangeConfiguration(
36 maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions
39 maybeUpdater.onConfigChange();
42 const typeHints = createHintStyle("type");
43 const paramHints = createHintStyle("parameter");
44 const chainingHints = createHintStyle("chaining");
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];
55 const fg = new vscode.ThemeColor(`rust_analyzer.inlayHints.foreground.${hintKind}Hints`);
56 const bg = new vscode.ThemeColor(`rust_analyzer.inlayHints.background.${hintKind}Hints`);
58 decorationType: vscode.window.createTextEditorDecorationType({
64 textDecoration: ";font-size:smaller",
67 toDecoration(hint: ra.InlayHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
69 range: conv.asRange(hint.range),
70 renderOptions: { [pos]: { contentText: render(hint.label) } }
76 class HintsUpdater implements Disposable {
77 private sourceFiles = new Map<string, RustSourceFile>(); // map Uri -> RustSourceFile
78 private readonly disposables: Disposable[] = [];
80 constructor(private readonly ctx: Ctx) {
81 vscode.window.onDidChangeVisibleTextEditors(
82 this.onDidChangeVisibleTextEditors,
87 vscode.workspace.onDidChangeTextDocument(
88 this.onDidChangeTextDocument,
93 // Set up initial cache shape
94 ctx.visibleRustEditors.forEach(editor => this.sourceFiles.set(
95 editor.document.uri.toString(),
97 document: editor.document,
99 cachedDecorations: null
103 this.syncCacheAndRenderHints();
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());
112 onDidChangeTextDocument({ contentChanges, document }: vscode.TextDocumentChangeEvent) {
113 if (contentChanges.length === 0 || !isRustDocument(document)) return;
114 this.syncCacheAndRenderHints();
117 syncCacheAndRenderHints() {
118 this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => {
121 file.cachedDecorations = this.hintsToDecorations(hints);
123 for (const editor of this.ctx.visibleRustEditors) {
124 if (editor.document.uri.toString() === uri) {
125 this.renderDecorations(editor, file.cachedDecorations);
131 onDidChangeVisibleTextEditors() {
132 const newSourceFiles = new Map<string, RustSourceFile>();
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,
140 cachedDecorations: null
142 newSourceFiles.set(uri, file);
144 // No text documents changed, so we may try to use the cache
145 if (!file.cachedDecorations) {
146 const hints = await this.fetchHints(file);
149 file.cachedDecorations = this.hintsToDecorations(hints);
152 this.renderDecorations(editor, file.cachedDecorations);
155 // Cancel requests for no longer visible (disposed) source files
156 this.sourceFiles.forEach((file, uri) => {
157 if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel();
160 this.sourceFiles = newSourceFiles;
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);
169 private hintsToDecorations(hints: ra.InlayHint[]): InlaysDecorations {
170 const decorations: InlaysDecorations = { type: [], param: [], chaining: [] };
171 const conv = this.ctx.client.protocol2CodeConverter;
173 for (const hint of hints) {
175 case ra.InlayHint.Kind.TypeHint: {
176 decorations.type.push(typeHints.toDecoration(hint, conv));
179 case ra.InlayHint.Kind.ParamHint: {
180 decorations.param.push(paramHints.toDecoration(hint, conv));
183 case ra.InlayHint.Kind.ChainingHint: {
184 decorations.chaining.push(chainingHints.toDecoration(hint, conv));
192 private async fetchHints(file: RustSourceFile): Promise<null | ra.InlayHint[]> {
193 file.inlaysRequest?.cancel();
195 const tokenSource = new vscode.CancellationTokenSource();
196 file.inlaysRequest = tokenSource;
198 const request = { textDocument: { uri: file.document.uri.toString() } };
200 return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token)
203 if (file.inlaysRequest === tokenSource) {
204 file.inlaysRequest = null;
210 interface InlaysDecorations {
211 type: vscode.DecorationOptions[];
212 param: vscode.DecorationOptions[];
213 chaining: vscode.DecorationOptions[];
216 interface RustSourceFile {
218 * Source of the token to cancel in-flight inlay hints request if any.
220 inlaysRequest: null | vscode.CancellationTokenSource;
222 * Last applied decorations.
224 cachedDecorations: null | InlaysDecorations;
226 document: RustDocument;