Skip to content

Commit de68137

Browse files
authored
feat: inlay hints support (#335)
close #300
1 parent 7b363e2 commit de68137

File tree

6 files changed

+303
-1
lines changed

6 files changed

+303
-1
lines changed

package.json

+105-1
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,111 @@
730730
"javascript.suggest.includeAutomaticOptionalChainCompletions": {
731731
"type": "boolean",
732732
"default": true,
733-
"description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%",
733+
"description": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires TS 3.7+ and strict null checks to be enabled.",
734+
"scope": "resource"
735+
},
736+
"typescript.inlayHints.parameterNames.enabled": {
737+
"type": "string",
738+
"enum": [
739+
"none",
740+
"literals",
741+
"all"
742+
],
743+
"enumDescriptions": [
744+
"Disable parameter name hints.",
745+
"Enable parameter name hints only for literal arguments.",
746+
"Enable parameter name hints for literal and non-literal arguments."
747+
],
748+
"default": "none",
749+
"description": "Enable/disable inlay hints of parameter names.",
750+
"scope": "resource"
751+
},
752+
"typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": {
753+
"type": "boolean",
754+
"default": true,
755+
"description": "Suppress parameter name hints on arguments whose text is identical to the parameter name.",
756+
"scope": "resource"
757+
},
758+
"typescript.inlayHints.parameterTypes.enabled": {
759+
"type": "boolean",
760+
"default": false,
761+
"description": "Enable/disable inlay hints of parameter types.",
762+
"scope": "resource"
763+
},
764+
"typescript.inlayHints.variableTypes.enabled": {
765+
"type": "boolean",
766+
"default": false,
767+
"description": "Enable/disable inlay hints of variable types.",
768+
"scope": "resource"
769+
},
770+
"typescript.inlayHints.propertyDeclarationTypes.enabled": {
771+
"type": "boolean",
772+
"default": false,
773+
"description": "Enable/disable inlay hints of property declarations.",
774+
"scope": "resource"
775+
},
776+
"typescript.inlayHints.functionLikeReturnTypes.enabled": {
777+
"type": "boolean",
778+
"default": false,
779+
"description": "Enable/disable inlay hints of return type for function signatures.",
780+
"scope": "resource"
781+
},
782+
"typescript.inlayHints.enumMemberValues.enabled": {
783+
"type": "boolean",
784+
"default": false,
785+
"description": "Enable/disable inlay hints of enum member values.",
786+
"scope": "resource"
787+
},
788+
"javascript.inlayHints.parameterNames.enabled": {
789+
"type": "string",
790+
"enum": [
791+
"none",
792+
"literals",
793+
"all"
794+
],
795+
"enumDescriptions": [
796+
"Disable parameter name hints.",
797+
"Enable parameter name hints only for literal arguments.",
798+
"Enable parameter name hints for literal and non-literal arguments."
799+
],
800+
"default": "none",
801+
"description": "Enable/disable inlay hints of parameter names.",
802+
"scope": "resource"
803+
},
804+
"javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": {
805+
"type": "boolean",
806+
"default": true,
807+
"description": "Suppress parameter name hints on arguments whose text is identical to the parameter name.",
808+
"scope": "resource"
809+
},
810+
"javascript.inlayHints.parameterTypes.enabled": {
811+
"type": "boolean",
812+
"default": false,
813+
"description": "Enable/disable inlay hints of parameter types.",
814+
"scope": "resource"
815+
},
816+
"javascript.inlayHints.variableTypes.enabled": {
817+
"type": "boolean",
818+
"default": false,
819+
"description": "Enable/disable inlay hints of variable types.",
820+
"scope": "resource"
821+
},
822+
"javascript.inlayHints.propertyDeclarationTypes.enabled": {
823+
"type": "boolean",
824+
"default": false,
825+
"description": "Enable/disable inlay hints of property declarations.",
826+
"scope": "resource"
827+
},
828+
"javascript.inlayHints.functionLikeReturnTypes.enabled": {
829+
"type": "boolean",
830+
"default": false,
831+
"description": "Enable/disable inlay hints of return type for function signatures.",
832+
"scope": "resource"
833+
},
834+
"javascript.inlayHints.enumMemberValues.enabled": {
835+
"type": "boolean",
836+
"default": false,
837+
"description": "Enable/disable inlay hints of enum member values.",
734838
"scope": "resource"
735839
},
736840
"javascript.autoClosingTags": {

src/server/features/fileConfigurationManager.ts

+32
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export default class FileConfigurationManager {
193193
includeCompletionsWithSnippetText: suggestConfig.includeCompletionsWithSnippetText,
194194
allowIncompleteCompletions: true,
195195
displayPartsForJSDoc: true,
196+
...getInlayHintsPreferences(language),
196197
}
197198
return preferences
198199
}
@@ -240,3 +241,34 @@ function getJsxAttributeCompletionStyle(config: WorkspaceConfiguration) {
240241
default: return 'auto'
241242
}
242243
}
244+
245+
export class InlayHintSettingNames {
246+
static readonly parameterNamesSuppressWhenArgumentMatchesName = 'inlayHints.parameterNames.suppressWhenArgumentMatchesName'
247+
static readonly parameterNamesEnabled = 'inlayHints.parameterTypes.enabled'
248+
static readonly variableTypesEnabled = 'inlayHints.variableTypes.enabled'
249+
static readonly propertyDeclarationTypesEnabled = 'inlayHints.propertyDeclarationTypes.enabled'
250+
static readonly functionLikeReturnTypesEnabled = 'inlayHints.functionLikeReturnTypes.enabled'
251+
static readonly enumMemberValuesEnabled = 'inlayHints.enumMemberValues.enabled'
252+
}
253+
254+
export function getInlayHintsPreferences(language: string) {
255+
const config = workspace.getConfiguration(language)
256+
return {
257+
includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config),
258+
includeInlayParameterNameHintsWhenArgumentMatchesName: !config.get<boolean>(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true),
259+
includeInlayFunctionParameterTypeHints: config.get<boolean>(InlayHintSettingNames.parameterNamesEnabled, false),
260+
includeInlayVariableTypeHints: config.get<boolean>(InlayHintSettingNames.variableTypesEnabled, false),
261+
includeInlayPropertyDeclarationTypeHints: config.get<boolean>(InlayHintSettingNames.propertyDeclarationTypesEnabled, false),
262+
includeInlayFunctionLikeReturnTypeHints: config.get<boolean>(InlayHintSettingNames.functionLikeReturnTypesEnabled, false),
263+
includeInlayEnumMemberValueHints: config.get<boolean>(InlayHintSettingNames.enumMemberValuesEnabled, false),
264+
} as const
265+
}
266+
267+
function getInlayParameterNameHintsPreference(config: WorkspaceConfiguration) {
268+
switch (config.get<string>('inlayHints.parameterNames.enabled')) {
269+
case 'none': return 'none'
270+
case 'literals': return 'literals'
271+
case 'all': return 'all'
272+
default: return undefined
273+
}
274+
}

src/server/features/inlayHints.ts

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { CancellationToken, CancellationTokenSource, Disposable, disposeAll, Document, events, Position, Range, TextDocument, workspace } from 'coc.nvim'
7+
import type * as Proto from '../protocol'
8+
import { ITypeScriptServiceClient } from '../typescriptService'
9+
import API from '../utils/api'
10+
import FileConfigurationManager, { getInlayHintsPreferences } from './fileConfigurationManager'
11+
12+
export enum InlayHintKind {
13+
Other = 0,
14+
Type = 1,
15+
Parameter = 2
16+
}
17+
18+
export interface InlayHint {
19+
text: string
20+
position: Position
21+
kind: InlayHintKind
22+
whitespaceBefore?: boolean
23+
whitespaceAfter?: boolean
24+
}
25+
26+
export default class TypeScriptInlayHintsProvider implements Disposable {
27+
public static readonly minVersion = API.v440
28+
private readonly inlayHintsNS = workspace.createNameSpace('tsserver-inlay-hint')
29+
30+
private _disposables: Disposable[] = []
31+
private _tokenSource: CancellationTokenSource | undefined = undefined
32+
private _inlayHints: Map<string, InlayHint[]> = new Map()
33+
34+
public dispose() {
35+
if (this._tokenSource) {
36+
this._tokenSource.cancel()
37+
this._tokenSource.dispose()
38+
this._tokenSource = undefined
39+
}
40+
41+
disposeAll(this._disposables)
42+
this._disposables = []
43+
this._inlayHints.clear()
44+
}
45+
46+
constructor(private readonly client: ITypeScriptServiceClient, private readonly fileConfigurationManager: FileConfigurationManager) {
47+
events.on('InsertLeave', async bufnr => {
48+
const doc = workspace.getDocument(bufnr)
49+
await this.syncAndRenderHints(doc)
50+
}, this, this._disposables)
51+
52+
workspace.onDidOpenTextDocument(async e => {
53+
const doc = workspace.getDocument(e.bufnr)
54+
await this.syncAndRenderHints(doc)
55+
}, this, this._disposables)
56+
57+
workspace.onDidChangeTextDocument(async e => {
58+
const doc = workspace.getDocument(e.bufnr)
59+
await this.syncAndRenderHints(doc)
60+
}, this, this._disposables)
61+
62+
this.syncAndRenderHints()
63+
}
64+
65+
private async syncAndRenderHints(doc?: Document) {
66+
if (!doc) doc = await workspace.document
67+
if (!isESDocument(doc)) return
68+
69+
if (this._tokenSource) {
70+
this._tokenSource.cancel()
71+
this._tokenSource.dispose()
72+
}
73+
74+
try {
75+
this._tokenSource = new CancellationTokenSource()
76+
const { token } = this._tokenSource
77+
const range = Range.create(0, 0, doc.lineCount, doc.getline(doc.lineCount).length)
78+
const hints = await this.provideInlayHints(doc.textDocument, range, token)
79+
if (token.isCancellationRequested) return
80+
81+
await this.renderHints(doc, hints)
82+
} catch (e) {
83+
console.error(e)
84+
this._tokenSource.cancel()
85+
this._tokenSource.dispose()
86+
}
87+
}
88+
89+
private async renderHints(doc: Document, hints: InlayHint[]) {
90+
this._inlayHints.set(doc.uri, hints)
91+
92+
const chaining_hints = {}
93+
for (const item of hints) {
94+
const chunks: [[string, string]] = [[item.text, 'CocHintSign']]
95+
if (chaining_hints[item.position.line] === undefined) {
96+
chaining_hints[item.position.line] = chunks
97+
} else {
98+
chaining_hints[item.position.line].push([' ', 'Normal'])
99+
chaining_hints[item.position.line].push(chunks[0])
100+
}
101+
}
102+
103+
doc.buffer.clearNamespace(this.inlayHintsNS)
104+
Object.keys(chaining_hints).forEach(async (line) => {
105+
await doc.buffer.setVirtualText(this.inlayHintsNS, Number(line), chaining_hints[line], {})
106+
})
107+
}
108+
109+
private inlayHintsEnabled(language: string) {
110+
const preferences = getInlayHintsPreferences(language)
111+
return preferences.includeInlayParameterNameHints === 'literals'
112+
|| preferences.includeInlayParameterNameHints === 'all'
113+
|| preferences.includeInlayEnumMemberValueHints
114+
|| preferences.includeInlayFunctionLikeReturnTypeHints
115+
|| preferences.includeInlayFunctionParameterTypeHints
116+
|| preferences.includeInlayPropertyDeclarationTypeHints
117+
|| preferences.includeInlayVariableTypeHints
118+
}
119+
120+
async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise<InlayHint[]> {
121+
if (!this.inlayHintsEnabled(document.languageId)) return []
122+
123+
const filepath = this.client.toOpenedFilePath(document.uri)
124+
if (!filepath) return []
125+
126+
const start = document.offsetAt(range.start)
127+
const length = document.offsetAt(range.end) - start
128+
129+
await this.fileConfigurationManager.ensureConfigurationForDocument(document, token)
130+
131+
const response = await this.client.execute('provideInlayHints', { file: filepath, start, length }, token)
132+
if (response.type !== 'response' || !response.success || !response.body) {
133+
return []
134+
}
135+
136+
return response.body.map(hint => {
137+
return {
138+
text: hint.text,
139+
position: Position.create(hint.position.line - 1, hint.position.offset - 1),
140+
kind: hint.kind && fromProtocolInlayHintKind(hint.kind),
141+
whitespaceAfter: hint.whitespaceAfter,
142+
whitespaceBefore: hint.whitespaceBefore,
143+
}
144+
})
145+
}
146+
}
147+
148+
function isESDocument(doc: Document) {
149+
if (!doc || !doc.attached) return false
150+
return doc.filetype === 'typescript' || doc.filetype === 'javascript'
151+
}
152+
153+
function fromProtocolInlayHintKind(kind: Proto.InlayHintKind): InlayHintKind {
154+
switch (kind) {
155+
case 'Parameter': return InlayHintKind.Parameter
156+
case 'Type': return InlayHintKind.Type
157+
case 'Enum': return InlayHintKind.Other
158+
default: return InlayHintKind.Other
159+
}
160+
}

src/server/languageProvider.ts

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import SignatureHelpProvider from './features/signatureHelp'
2929
import SemanticTokensProvider from './features/semanticTokens'
3030
import SmartSelection from './features/smartSelect'
3131
import TagClosing from './features/tagClosing'
32+
import TypeScriptInlayHintsProvider from './features/inlayHints'
3233
import UpdateImportsOnFileRenameHandler from './features/updatePathOnRename'
3334
import { OrganizeImportsCodeActionProvider } from './organizeImports'
3435
import TypeScriptServiceClient from './typescriptServiceClient'
@@ -159,6 +160,9 @@ export default class LanguageProvider {
159160
if (this.client.apiVersion.gte(API.v300)) {
160161
this._register(new TagClosing(this.client, this.description.id))
161162
}
163+
if (this.client.apiVersion.gte(API.v440) && workspace.isNvim) {
164+
this._register(new TypeScriptInlayHintsProvider(this.client, this.fileConfigurationManager))
165+
}
162166
}
163167

164168
public handles(resource: string, doc: TextDocument): boolean {

src/server/typescriptService.ts

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export interface TypeScriptRequestTypes {
8484
'provideCallHierarchyIncomingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyIncomingCallsResponse]
8585
'provideCallHierarchyOutgoingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyOutgoingCallsResponse]
8686
'fileReferences': [Proto.FileRequestArgs, Proto.FileReferencesResponse]
87+
'provideInlayHints': [Proto.InlayHintsRequestArgs, Proto.InlayHintsResponse]
8788
}
8889

8990
export interface ITypeScriptServiceClient {

src/server/utils/api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export default class API {
4343
public static readonly v401 = API.fromSimpleString('4.0.1')
4444
public static readonly v420 = API.fromSimpleString('4.2.0')
4545
public static readonly v430 = API.fromSimpleString('4.3.0')
46+
public static readonly v440 = API.fromSimpleString('4.4.0')
4647

4748
public static fromVersionString(versionString: string): API {
4849
let version = semver.valid(versionString)

0 commit comments

Comments
 (0)