Skip to content

Commit c7793ba

Browse files
committed
Implement inlay hints request for VSCode
Implement the `sourcekit-lsp/inlayHints` request on the client side for VSCode. - Add scaffolding for inlay hints in the VSCode extension - Add InlayHintsStyle - Add some notes on the inlay hints client - Implement generation of inlay hint decorations for VSCode - Implement syncCacheAndRenderHints on the VSCode side - Consistently use 4-space-indent in TypeScript sources - Remove unused (and unneeded) hints check - Address suggestions regarding inlay hints in VSCode
1 parent 7a2d760 commit c7793ba

File tree

4 files changed

+255
-2
lines changed

4 files changed

+255
-2
lines changed

Editors/vscode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
"default": "",
5959
"description": "(optional) The path of the swift toolchain. By default, sourcekit-lsp uses the toolchain it is installed in."
6060
},
61+
"sourcekit-lsp.inlayHints.enabled": {
62+
"type": "boolean",
63+
"default": true,
64+
"description": "Render inlay type annotations in the editor."
65+
},
6166
"sourcekit-lsp.trace.server": {
6267
"type": "string",
6368
"default": "off",

Editors/vscode/src/extension.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use strict';
22
import * as vscode from 'vscode';
33
import * as langclient from 'vscode-languageclient/node';
4+
import { activateInlayHints } from './inlayHints';
45

5-
export function activate(context: vscode.ExtensionContext) {
6-
6+
export async function activate(context: vscode.ExtensionContext): Promise<void> {
77
const config = vscode.workspace.getConfiguration('sourcekit-lsp');
88

99
const sourcekit: langclient.Executable = {
@@ -35,6 +35,9 @@ export function activate(context: vscode.ExtensionContext) {
3535
context.subscriptions.push(client.start());
3636

3737
console.log('SourceKit-LSP is now active!');
38+
39+
await client.onReady();
40+
activateInlayHints(context, client);
3841
}
3942

4043
export function deactivate() {

Editors/vscode/src/inlayHints.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
'use strict';
2+
import * as vscode from 'vscode';
3+
import * as langclient from 'vscode-languageclient/node';
4+
import { InlayHint, InlayHintsParams, inlayHintsRequest } from './lspExtensions';
5+
6+
// The implementation is loosely based on the rust-analyzer implementation
7+
// of inlay hints: https://github.com/rust-analyzer/rust-analyzer/blob/master/editors/code/src/inlay_hints.ts
8+
9+
// Note that once support for inlay hints is officially added to LSP/VSCode,
10+
// this module providing custom decorations will no longer be needed!
11+
12+
export async function activateInlayHints(
13+
context: vscode.ExtensionContext,
14+
client: langclient.LanguageClient
15+
): Promise<void> {
16+
const config = vscode.workspace.getConfiguration('sourcekit-lsp');
17+
let updater: HintsUpdater | null = null;
18+
19+
const onConfigChange = async () => {
20+
const wasEnabled = updater !== null;
21+
const isEnabled = config.get<boolean>('inlayHints.enabled', true);
22+
23+
if (wasEnabled !== isEnabled) {
24+
updater?.dispose();
25+
if (isEnabled) {
26+
updater = new HintsUpdater(client);
27+
} else {
28+
updater = null;
29+
}
30+
}
31+
};
32+
33+
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(onConfigChange));
34+
context.subscriptions.push({ dispose: () => updater?.dispose() });
35+
36+
onConfigChange().catch(console.error);
37+
}
38+
39+
interface InlayHintStyle {
40+
decorationType: vscode.TextEditorDecorationType;
41+
42+
makeDecoration(hint: InlayHint, converter: langclient.Protocol2CodeConverter): vscode.DecorationOptions;
43+
}
44+
45+
const hintStyle: InlayHintStyle = {
46+
decorationType: vscode.window.createTextEditorDecorationType({
47+
after: {
48+
color: new vscode.ThemeColor('editorCodeLens.foreground'),
49+
fontStyle: 'normal',
50+
fontWeight: 'normal'
51+
}
52+
}),
53+
54+
makeDecoration: (hint, converter) => ({
55+
range: converter.asRange({
56+
start: hint.position,
57+
end: { ...hint.position, character: hint.position.character + 1 } }
58+
),
59+
renderOptions: {
60+
after: {
61+
// U+200C is a zero-width non-joiner to prevent the editor from
62+
// forming a ligature between the code and an inlay hint.
63+
contentText: `\u{200c}: ${hint.label}`
64+
}
65+
}
66+
})
67+
};
68+
69+
interface SourceFile {
70+
/** Source of the token for cancelling in-flight inlay hint requests. */
71+
inFlightInlayHints: null | vscode.CancellationTokenSource;
72+
73+
/** Most recently applied decorations. */
74+
cachedDecorations: null | vscode.DecorationOptions[];
75+
76+
/** The source file document in question. */
77+
document: vscode.TextDocument;
78+
}
79+
80+
class HintsUpdater implements vscode.Disposable {
81+
private readonly disposables: vscode.Disposable[] = [];
82+
private sourceFiles: Map<string, SourceFile> = new Map(); // uri -> SourceFile
83+
84+
constructor(private readonly client: langclient.LanguageClient) {
85+
// Register listeners
86+
vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
87+
vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables);
88+
89+
// Set up initial cache
90+
this.visibleSourceKitLSPEditors.forEach(editor => this.sourceFiles.set(
91+
editor.document.uri.toString(),
92+
{
93+
document: editor.document,
94+
inFlightInlayHints: null,
95+
cachedDecorations: null
96+
}
97+
));
98+
99+
this.syncCacheAndRenderHints();
100+
}
101+
102+
private onDidChangeVisibleTextEditors(): void {
103+
const newSourceFiles = new Map<string, SourceFile>();
104+
105+
// Rerender all, even up-to-date editors for simplicity
106+
this.visibleSourceKitLSPEditors.forEach(async editor => {
107+
const uri = editor.document.uri.toString();
108+
const file = this.sourceFiles.get(uri) ?? {
109+
document: editor.document,
110+
inFlightInlayHints: null,
111+
cachedDecorations: null
112+
};
113+
newSourceFiles.set(uri, file);
114+
115+
// No text documents changed, so we may try to use the cache
116+
if (!file.cachedDecorations) {
117+
const hints = await this.fetchHints(file);
118+
file.cachedDecorations = this.hintsToDecorations(hints);
119+
}
120+
121+
this.renderDecorations(editor, file.cachedDecorations);
122+
});
123+
124+
// Cancel requests for no longer visible (disposed) source files
125+
this.sourceFiles.forEach((file, uri) => {
126+
if (!newSourceFiles.has(uri)) {
127+
file.inFlightInlayHints?.cancel();
128+
}
129+
});
130+
131+
this.sourceFiles = newSourceFiles;
132+
}
133+
134+
private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent): void {
135+
if (event.contentChanges.length !== 0 && this.isSourceKitLSPDocument(event.document)) {
136+
this.syncCacheAndRenderHints();
137+
}
138+
}
139+
140+
private syncCacheAndRenderHints(): void {
141+
this.sourceFiles.forEach(async (file, uri) => {
142+
const hints = await this.fetchHints(file);
143+
144+
const decorations = this.hintsToDecorations(hints);
145+
file.cachedDecorations = decorations;
146+
147+
this.visibleSourceKitLSPEditors.forEach(editor => {
148+
if (editor.document.uri.toString() === uri) {
149+
this.renderDecorations(editor, decorations);
150+
}
151+
});
152+
});
153+
}
154+
155+
private get visibleSourceKitLSPEditors(): vscode.TextEditor[] {
156+
return vscode.window.visibleTextEditors.filter(e => this.isSourceKitLSPDocument(e.document));
157+
}
158+
159+
private isSourceKitLSPDocument(document: vscode.TextDocument): boolean {
160+
// TODO: Add other SourceKit-LSP languages if/once we forward inlay
161+
// hint requests to clangd.
162+
return document.languageId === 'swift' && document.uri.scheme === 'file';
163+
}
164+
165+
private renderDecorations(editor: vscode.TextEditor, decorations: vscode.DecorationOptions[]): void {
166+
editor.setDecorations(hintStyle.decorationType, decorations);
167+
}
168+
169+
private hintsToDecorations(hints: InlayHint[]): vscode.DecorationOptions[] {
170+
const converter = this.client.protocol2CodeConverter;
171+
return hints.map(h => hintStyle.makeDecoration(h, converter));
172+
}
173+
174+
private async fetchHints(file: SourceFile): Promise<InlayHint[]> {
175+
file.inFlightInlayHints?.cancel();
176+
177+
const tokenSource = new vscode.CancellationTokenSource();
178+
file.inFlightInlayHints = tokenSource;
179+
180+
// TODO: Specify a range
181+
const params: InlayHintsParams = {
182+
textDocument: { uri: file.document.uri.toString() }
183+
}
184+
185+
try {
186+
return await this.client.sendRequest(inlayHintsRequest, params, tokenSource.token);
187+
} catch (e) {
188+
this.client.outputChannel.appendLine(`Could not fetch inlay hints: ${e}`);
189+
return [];
190+
} finally {
191+
if (file.inFlightInlayHints.token === tokenSource.token) {
192+
file.inFlightInlayHints = null;
193+
}
194+
}
195+
}
196+
197+
dispose(): void {
198+
this.disposables.forEach(d => d.dispose());
199+
}
200+
}

Editors/vscode/src/lspExtensions.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
import * as langclient from 'vscode-languageclient/node';
3+
4+
// Definitions for non-standard requests used by sourcekit-lsp
5+
6+
export interface InlayHintsParams {
7+
/**
8+
* The text document.
9+
*/
10+
textDocument: langclient.TextDocumentIdentifier;
11+
12+
/**
13+
* If set, the reange for which inlay hints are
14+
* requested. If unset, hints for the entire document
15+
* are returned.
16+
*/
17+
range?: langclient.Range;
18+
19+
/**
20+
* The categories of inlay hints that are requested.
21+
* If unset, all categories are returned.
22+
*/
23+
only?: string[];
24+
}
25+
26+
export interface InlayHint {
27+
/**
28+
* The position within the code that this hint is
29+
* attached to.
30+
*/
31+
position: langclient.Position;
32+
33+
/**
34+
* The hint's kind, used for more flexible client-side
35+
* styling of the hint.
36+
*/
37+
category?: string;
38+
39+
/**
40+
* The hint's rendered label.
41+
*/
42+
label: string;
43+
}
44+
45+
export const inlayHintsRequest = new langclient.RequestType<InlayHintsParams, InlayHint[], unknown>('sourcekit-lsp/inlayHints');

0 commit comments

Comments
 (0)