aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--editors/code/src/ctx.ts12
-rw-r--r--editors/code/src/inlay_hints.ts290
-rw-r--r--editors/code/src/rust-analyzer-api.ts22
-rw-r--r--editors/code/src/util.ts12
4 files changed, 202 insertions, 134 deletions
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index b4e983a0c..25ef38aed 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -3,7 +3,7 @@ import * as lc from 'vscode-languageclient';
3 3
4import { Config } from './config'; 4import { Config } from './config';
5import { createClient } from './client'; 5import { createClient } from './client';
6import { isRustDocument } from './util'; 6import { isRustEditor, RustEditor } from './util';
7 7
8export class Ctx { 8export class Ctx {
9 private constructor( 9 private constructor(
@@ -22,17 +22,15 @@ export class Ctx {
22 return res; 22 return res;
23 } 23 }
24 24
25 get activeRustEditor(): vscode.TextEditor | undefined { 25 get activeRustEditor(): RustEditor | undefined {
26 const editor = vscode.window.activeTextEditor; 26 const editor = vscode.window.activeTextEditor;
27 return editor && isRustDocument(editor.document) 27 return editor && isRustEditor(editor)
28 ? editor 28 ? editor
29 : undefined; 29 : undefined;
30 } 30 }
31 31
32 get visibleRustEditors(): vscode.TextEditor[] { 32 get visibleRustEditors(): RustEditor[] {
33 return vscode.window.visibleTextEditors.filter( 33 return vscode.window.visibleTextEditors.filter(isRustEditor);
34 editor => isRustDocument(editor.document),
35 );
36 } 34 }
37 35
38 registerCommand(name: string, factory: (ctx: Ctx) => Cmd) { 36 registerCommand(name: string, factory: (ctx: Ctx) => Cmd) {
diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts
index 08d3a64a7..e1a82e03e 100644
--- a/editors/code/src/inlay_hints.ts
+++ b/editors/code/src/inlay_hints.ts
@@ -1,156 +1,214 @@
1import * as lc from "vscode-languageclient";
1import * as vscode from 'vscode'; 2import * as vscode from 'vscode';
2import * as ra from './rust-analyzer-api'; 3import * as ra from './rust-analyzer-api';
3 4
4import { Ctx } from './ctx'; 5import { Ctx, Disposable } from './ctx';
5import { log, sendRequestWithRetry, isRustDocument } from './util'; 6import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor } from './util';
6 7
7export function activateInlayHints(ctx: Ctx) {
8 const hintsUpdater = new HintsUpdater(ctx);
9 vscode.window.onDidChangeVisibleTextEditors(
10 async _ => hintsUpdater.refresh(),
11 null,
12 ctx.subscriptions
13 );
14 8
15 vscode.workspace.onDidChangeTextDocument( 9export function activateInlayHints(ctx: Ctx) {
16 async event => { 10 const maybeUpdater = {
17 if (event.contentChanges.length === 0) return; 11 updater: null as null | HintsUpdater,
18 if (!isRustDocument(event.document)) return; 12 onConfigChange() {
19 await hintsUpdater.refresh(); 13 if (!ctx.config.displayInlayHints) {
14 return this.dispose();
15 }
16 if (!this.updater) this.updater = new HintsUpdater(ctx);
20 }, 17 },
21 null, 18 dispose() {
22 ctx.subscriptions 19 this.updater?.dispose();
23 ); 20 this.updater = null;
21 }
22 };
23
24 ctx.pushCleanup(maybeUpdater);
24 25
25 vscode.workspace.onDidChangeConfiguration( 26 vscode.workspace.onDidChangeConfiguration(
26 async _ => hintsUpdater.setEnabled(ctx.config.displayInlayHints), 27 maybeUpdater.onConfigChange, maybeUpdater, ctx.subscriptions
27 null,
28 ctx.subscriptions
29 ); 28 );
30 29
31 ctx.pushCleanup({ 30 maybeUpdater.onConfigChange();
32 dispose() { 31}
33 hintsUpdater.clear(); 32
33
34const typeHints = {
35 decorationType: vscode.window.createTextEditorDecorationType({
36 after: {
37 color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
38 fontStyle: "normal",
34 } 39 }
35 }); 40 }),
36 41
37 // XXX: we don't await this, thus Promise rejections won't be handled, but 42 toDecoration(hint: ra.InlayHint.TypeHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
38 // this should never throw in fact... 43 return {
39 void hintsUpdater.setEnabled(ctx.config.displayInlayHints); 44 range: conv.asRange(hint.range),
40} 45 renderOptions: { after: { contentText: `: ${hint.label}` } }
46 };
47 }
48};
49
50const paramHints = {
51 decorationType: vscode.window.createTextEditorDecorationType({
52 before: {
53 color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
54 fontStyle: "normal",
55 }
56 }),
41 57
42const typeHintDecorationType = vscode.window.createTextEditorDecorationType({ 58 toDecoration(hint: ra.InlayHint.ParamHint, conv: lc.Protocol2CodeConverter): vscode.DecorationOptions {
43 after: { 59 return {
44 color: new vscode.ThemeColor('rust_analyzer.inlayHint'), 60 range: conv.asRange(hint.range),
45 fontStyle: "normal", 61 renderOptions: { before: { contentText: `${hint.label}: ` } }
46 }, 62 };
47});
48
49const parameterHintDecorationType = vscode.window.createTextEditorDecorationType({
50 before: {
51 color: new vscode.ThemeColor('rust_analyzer.inlayHint'),
52 fontStyle: "normal",
53 },
54});
55
56class HintsUpdater {
57 private pending = new Map<string, vscode.CancellationTokenSource>();
58 private ctx: Ctx;
59 private enabled: boolean;
60
61 constructor(ctx: Ctx) {
62 this.ctx = ctx;
63 this.enabled = false;
64 } 63 }
64};
65 65
66 async setEnabled(enabled: boolean): Promise<void> { 66class HintsUpdater implements Disposable {
67 log.debug({ enabled, prev: this.enabled }); 67 private sourceFiles = new Map<string, RustSourceFile>(); // map Uri -> RustSourceFile
68 private readonly disposables: Disposable[] = [];
68 69
69 if (this.enabled === enabled) return; 70 constructor(private readonly ctx: Ctx) {
70 this.enabled = enabled; 71 vscode.window.onDidChangeVisibleTextEditors(
72 this.onDidChangeVisibleTextEditors,
73 this,
74 this.disposables
75 );
71 76
72 if (this.enabled) { 77 vscode.workspace.onDidChangeTextDocument(
73 return await this.refresh(); 78 this.onDidChangeTextDocument,
74 } else { 79 this,
75 return this.clear(); 80 this.disposables
76 } 81 );
82
83 // Set up initial cache shape
84 ctx.visibleRustEditors.forEach(editor => this.sourceFiles.set(
85 editor.document.uri.toString(),
86 {
87 document: editor.document,
88 inlaysRequest: null,
89 cachedDecorations: null
90 }
91 ));
92
93 this.syncCacheAndRenderHints();
77 } 94 }
78 95
79 clear() { 96 dispose() {
80 this.ctx.visibleRustEditors.forEach(it => { 97 this.sourceFiles.forEach(file => file.inlaysRequest?.cancel());
81 this.setTypeDecorations(it, []); 98 this.ctx.visibleRustEditors.forEach(editor => this.renderDecorations(editor, { param: [], type: [] }));
82 this.setParameterDecorations(it, []); 99 this.disposables.forEach(d => d.dispose());
83 }); 100 }
101
102 onDidChangeTextDocument({ contentChanges, document }: vscode.TextDocumentChangeEvent) {
103 if (contentChanges.length === 0 || !isRustDocument(document)) return;
104 this.syncCacheAndRenderHints();
84 } 105 }
85 106
86 async refresh() { 107 private syncCacheAndRenderHints() {
87 if (!this.enabled) return; 108 // FIXME: make inlayHints request pass an array of files?
88 await Promise.all(this.ctx.visibleRustEditors.map(it => this.refreshEditor(it))); 109 this.sourceFiles.forEach((file, uri) => this.fetchHints(file).then(hints => {
110 if (!hints) return;
111
112 file.cachedDecorations = this.hintsToDecorations(hints);
113
114 for (const editor of this.ctx.visibleRustEditors) {
115 if (editor.document.uri.toString() === uri) {
116 this.renderDecorations(editor, file.cachedDecorations);
117 }
118 }
119 }));
89 } 120 }
90 121
91 private async refreshEditor(editor: vscode.TextEditor): Promise<void> { 122 onDidChangeVisibleTextEditors() {
92 const newHints = await this.queryHints(editor.document.uri.toString()); 123 const newSourceFiles = new Map<string, RustSourceFile>();
93 if (newHints == null) return; 124
94 125 // Rerendering all, even up-to-date editors for simplicity
95 const newTypeDecorations = newHints 126 this.ctx.visibleRustEditors.forEach(async editor => {
96 .filter(hint => hint.kind === ra.InlayKind.TypeHint) 127 const uri = editor.document.uri.toString();
97 .map(hint => ({ 128 const file = this.sourceFiles.get(uri) ?? {
98 range: this.ctx.client.protocol2CodeConverter.asRange(hint.range), 129 document: editor.document,
99 renderOptions: { 130 inlaysRequest: null,
100 after: { 131 cachedDecorations: null
101 contentText: `: ${hint.label}`, 132 };
102 }, 133 newSourceFiles.set(uri, file);
103 }, 134
104 })); 135 // No text documents changed, so we may try to use the cache
105 this.setTypeDecorations(editor, newTypeDecorations); 136 if (!file.cachedDecorations) {
106 137 file.inlaysRequest?.cancel();
107 const newParameterDecorations = newHints 138
108 .filter(hint => hint.kind === ra.InlayKind.ParameterHint) 139 const hints = await this.fetchHints(file);
109 .map(hint => ({ 140 if (!hints) return;
110 range: this.ctx.client.protocol2CodeConverter.asRange(hint.range), 141
111 renderOptions: { 142 file.cachedDecorations = this.hintsToDecorations(hints);
112 before: { 143 }
113 contentText: `${hint.label}: `, 144
114 }, 145 this.renderDecorations(editor, file.cachedDecorations);
115 }, 146 });
116 })); 147
117 this.setParameterDecorations(editor, newParameterDecorations); 148 // Cancel requests for no longer visible (disposed) source files
149 this.sourceFiles.forEach((file, uri) => {
150 if (!newSourceFiles.has(uri)) file.inlaysRequest?.cancel();
151 });
152
153 this.sourceFiles = newSourceFiles;
118 } 154 }
119 155
120 private setTypeDecorations( 156 private renderDecorations(editor: RustEditor, decorations: InlaysDecorations) {
121 editor: vscode.TextEditor, 157 editor.setDecorations(typeHints.decorationType, decorations.type);
122 decorations: vscode.DecorationOptions[], 158 editor.setDecorations(paramHints.decorationType, decorations.param);
123 ) {
124 editor.setDecorations(
125 typeHintDecorationType,
126 this.enabled ? decorations : [],
127 );
128 } 159 }
129 160
130 private setParameterDecorations( 161 private hintsToDecorations(hints: ra.InlayHint[]): InlaysDecorations {
131 editor: vscode.TextEditor, 162 const decorations: InlaysDecorations = { type: [], param: [] };
132 decorations: vscode.DecorationOptions[], 163 const conv = this.ctx.client.protocol2CodeConverter;
133 ) { 164
134 editor.setDecorations( 165 for (const hint of hints) {
135 parameterHintDecorationType, 166 switch (hint.kind) {
136 this.enabled ? decorations : [], 167 case ra.InlayHint.Kind.TypeHint: {
137 ); 168 decorations.type.push(typeHints.toDecoration(hint, conv));
169 continue;
170 }
171 case ra.InlayHint.Kind.ParamHint: {
172 decorations.param.push(paramHints.toDecoration(hint, conv));
173 continue;
174 }
175 }
176 }
177 return decorations;
138 } 178 }
139 179
140 private async queryHints(documentUri: string): Promise<ra.InlayHint[] | null> { 180 private async fetchHints(file: RustSourceFile): Promise<null | ra.InlayHint[]> {
141 this.pending.get(documentUri)?.cancel(); 181 file.inlaysRequest?.cancel();
142 182
143 const tokenSource = new vscode.CancellationTokenSource(); 183 const tokenSource = new vscode.CancellationTokenSource();
144 this.pending.set(documentUri, tokenSource); 184 file.inlaysRequest = tokenSource;
145 185
146 const request = { textDocument: { uri: documentUri } }; 186 const request = { textDocument: { uri: file.document.uri.toString() } };
147 187
148 return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token) 188 return sendRequestWithRetry(this.ctx.client, ra.inlayHints, request, tokenSource.token)
149 .catch(_ => null) 189 .catch(_ => null)
150 .finally(() => { 190 .finally(() => {
151 if (!tokenSource.token.isCancellationRequested) { 191 if (file.inlaysRequest === tokenSource) {
152 this.pending.delete(documentUri); 192 file.inlaysRequest = null;
153 } 193 }
154 }); 194 });
155 } 195 }
156} 196}
197
198interface InlaysDecorations {
199 type: vscode.DecorationOptions[];
200 param: vscode.DecorationOptions[];
201}
202
203interface RustSourceFile {
204 /*
205 * Source of the token to cancel in-flight inlay hints request if any.
206 */
207 inlaysRequest: null | vscode.CancellationTokenSource;
208 /**
209 * Last applied decorations.
210 */
211 cachedDecorations: null | InlaysDecorations;
212
213 document: RustDocument;
214}
diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts
index c5a010e94..bd6e3ada0 100644
--- a/editors/code/src/rust-analyzer-api.ts
+++ b/editors/code/src/rust-analyzer-api.ts
@@ -86,14 +86,20 @@ export interface Runnable {
86export const runnables = request<RunnablesParams, Vec<Runnable>>("runnables"); 86export const runnables = request<RunnablesParams, Vec<Runnable>>("runnables");
87 87
88 88
89export const enum InlayKind { 89
90 TypeHint = "TypeHint", 90export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint;
91 ParameterHint = "ParameterHint", 91
92} 92export namespace InlayHint {
93export interface InlayHint { 93 export const enum Kind {
94 range: lc.Range; 94 TypeHint = "TypeHint",
95 kind: InlayKind; 95 ParamHint = "ParameterHint",
96 label: string; 96 }
97 interface Common {
98 range: lc.Range;
99 label: string;
100 }
101 export type TypeHint = Common & { kind: Kind.TypeHint };
102 export type ParamHint = Common & { kind: Kind.ParamHint };
97} 103}
98export interface InlayHintsParams { 104export interface InlayHintsParams {
99 textDocument: lc.TextDocumentIdentifier; 105 textDocument: lc.TextDocumentIdentifier;
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 7c95769bb..95a5f1227 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -1,7 +1,6 @@
1import * as lc from "vscode-languageclient"; 1import * as lc from "vscode-languageclient";
2import * as vscode from "vscode"; 2import * as vscode from "vscode";
3import { strict as nativeAssert } from "assert"; 3import { strict as nativeAssert } from "assert";
4import { TextDocument } from "vscode";
5 4
6export function assert(condition: boolean, explanation: string): asserts condition { 5export function assert(condition: boolean, explanation: string): asserts condition {
7 try { 6 try {
@@ -67,9 +66,16 @@ function sleep(ms: number) {
67 return new Promise(resolve => setTimeout(resolve, ms)); 66 return new Promise(resolve => setTimeout(resolve, ms));
68} 67}
69 68
70export function isRustDocument(document: TextDocument) { 69export type RustDocument = vscode.TextDocument & { languageId: "rust" };
70export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string };
71
72export function isRustDocument(document: vscode.TextDocument): document is RustDocument {
71 return document.languageId === 'rust' 73 return document.languageId === 'rust'
72 // SCM diff views have the same URI as the on-disk document but not the same content 74 // SCM diff views have the same URI as the on-disk document but not the same content
73 && document.uri.scheme !== 'git' 75 && document.uri.scheme !== 'git'
74 && document.uri.scheme !== 'svn'; 76 && document.uri.scheme !== 'svn';
75} \ No newline at end of file 77}
78
79export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
80 return isRustDocument(editor.document);
81}