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

232 lines
7.4 KiB
TypeScript
Raw Normal View History

import * as lc from "vscode-languageclient";
2019-07-23 13:38:21 +00:00
import * as vscode from 'vscode';
import * as ra from './rust-analyzer-api';
2019-12-30 20:28:38 +00:00
import { Ctx, Disposable } from './ctx';
import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, log } from './util';
export function activateInlayHints(ctx: Ctx) {
const maybeUpdater = {
updater: null as null | HintsUpdater,
onConfigChange() {
if (!ctx.config.displayInlayHints) {
return this.dispose();
}
if (!this.updater) this.updater = HintsUpdater.create(ctx);
},
dispose() {
this.updater?.dispose();
this.updater = null;
}
};
ctx.pushCleanup(maybeUpdater);
vscode.workspace.onDidChangeConfiguration(
maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions
);
2019-12-30 20:28:38 +00:00
maybeUpdater.onConfigChange();
2019-12-30 19:21:25 +00:00
}
2019-07-23 13:38:21 +00:00
const typeHints = {
decorationType: vscode.window.createTextEditorDecorationType({
after: {
color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
fontStyle: "normal",
}
}),
toDecoration(hint: ra.InlayHint.TypeHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
return {
range: conv.asRange(hint.range),
renderOptions: { after: { contentText: `: ${hint.label}` } }
};
}
};
const paramHints = {
decorationType: vscode.window.createTextEditorDecorationType({
before: {
color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
fontStyle: "normal",
}
}),
2019-07-23 13:38:21 +00:00
toDecoration(hint: ra.InlayHint.ParamHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
return {
range: conv.asRange(hint.range),
renderOptions: { before: { contentText: `${hint.label}: ` } }
};
2019-08-05 19:31:12 +00:00
}
};
class HintsUpdater implements Disposable {
private sourceFiles = new Map<string, RustSourceFile>(); // map Uri -> RustSourceFile
private readonly disposables: Disposable[] = [];
2020-02-17 11:17:01 +00:00
private constructor(readonly ctx: Ctx) { }
2019-07-23 13:38:21 +00:00
static create(ctx: Ctx) {
const self = new HintsUpdater(ctx);
2020-02-16 23:47:14 +00:00
vscode.window.onDidChangeVisibleTextEditors(
self.onDidChangeVisibleTextEditors,
self,
self.disposables
);
vscode.workspace.onDidChangeTextDocument(
self.onDidChangeTextDocument,
self,
self.disposables
);
// Set up initial cache shape
ctx.visibleRustEditors.forEach(editor => self.sourceFiles.set(
editor.document.uri.toString(), {
document: editor.document,
inlaysRequest: null,
cachedDecorations: null
}
));
self.syncCacheAndRenderHints();
return self;
}
dispose() {
this.sourceFiles.forEach(file => file.inlaysRequest?.cancel());
this.ctx.visibleRustEditors.forEach(editor => this.renderDecorations(editor, { param: [], type: [] }));
this.disposables.forEach(d => d.dispose());
}
onDidChangeTextDocument({contentChanges, document}: vscode.TextDocumentChangeEvent) {
if (contentChanges.length === 0 || !isRustDocument(document)) return;
log.debug(`[inlays]: changed text doc!`);
this.syncCacheAndRenderHints();
}
private syncCacheAndRenderHints() {
// FIXME: make inlayHints request pass an array of files?
this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => {
if (!hints) return;
file.cachedDecorations = this.hintsToDecorations(hints);
for (const editor of this.ctx.visibleRustEditors) {
if (editor.document.uri.toString() === uri) {
this.renderDecorations(editor, file.cachedDecorations);
}
}
}));
}
onDidChangeVisibleTextEditors() {
log.debug(`[inlays]: changed visible text editors`);
const newSourceFiles = new Map<string, RustSourceFile>();
// Rerendering all, even up-to-date editors for simplicity
this.ctx.visibleRustEditors.forEach(async editor => {
const uri = editor.document.uri.toString();
const file = this.sourceFiles.get(uri) ?? {
document: editor.document,
inlaysRequest: null,
cachedDecorations: null
};
newSourceFiles.set(uri, file);
// No text documents changed, so we may try to use the cache
if (!file.cachedDecorations) {
file.inlaysRequest?.cancel();
const hints = await this.fetchHints(file);
if (!hints) return;
file.cachedDecorations = this.hintsToDecorations(hints);
}
this.renderDecorations(editor, file.cachedDecorations);
});
// Cancel requests for no longer visible (disposed) source files
this.sourceFiles.forEach((file, uri) => {
if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel();
});
this.sourceFiles = newSourceFiles;
}
private renderDecorations(editor: RustEditor, decorations: InlaysDecorations) {
editor.setDecorations(typeHints.decorationType, decorations.type);
editor.setDecorations(paramHints.decorationType, decorations.param);
}
private hintsToDecorations(hints: ra.InlayHint[]): InlaysDecorations {
const decorations: InlaysDecorations = { type: [], param: [] };
const conv = this.ctx.client.protocol2CodeConverter;
for (const hint of hints) {
switch (hint.kind) {
case ra.InlayHint.Kind.TypeHint: {
decorations.type.push(typeHints.toDecoration(hint, conv));
continue;
}
case ra.InlayHint.Kind.ParamHint: {
decorations.param.push(paramHints.toDecoration(hint, conv));
continue;
}
}
}
return decorations;
}
lastReqId = 0;
private async fetchHints(file: RustSourceFile): Promise<null | ra.InlayHint[]> {
const reqId = ++this.lastReqId;
log.debug(`[inlays]: ${reqId} requesting`);
file.inlaysRequest?.cancel();
const tokenSource = new vscode.CancellationTokenSource();
file.inlaysRequest = tokenSource;
const request = { textDocument: { uri: file.document.uri.toString() } };
return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token)
.catch(_ => {
log.debug(`[inlays]: ${reqId} err`);
return null;
})
.finally(() => {
if (file.inlaysRequest === tokenSource) {
file.inlaysRequest = null;
log.debug(`[inlays]: ${reqId} got response!`);
} else {
log.debug(`[inlays]: ${reqId} cancelled!`);
}
})
2019-07-23 13:38:21 +00:00
}
}
interface InlaysDecorations {
type: vscode.DecorationOptions[];
param: vscode.DecorationOptions[];
}
interface RustSourceFile {
/*
* Source of the token to cancel in-flight inlay hints request if any.
*/
inlaysRequest: null | vscode.CancellationTokenSource;
/**
* Last applied decorations.
*/
cachedDecorations: null | InlaysDecorations;
document: RustDocument
}