diff --git a/package.json b/package.json index 0e8e29e..6369768 100644 --- a/package.json +++ b/package.json @@ -730,7 +730,111 @@ "javascript.suggest.includeAutomaticOptionalChainCompletions": { "type": "boolean", "default": true, - "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", + "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.", + "scope": "resource" + }, + "typescript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "Disable parameter name hints.", + "Enable parameter name hints only for literal arguments.", + "Enable parameter name hints for literal and non-literal arguments." + ], + "default": "none", + "description": "Enable/disable inlay hints of parameter names.", + "scope": "resource" + }, + "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "description": "Suppress parameter name hints on arguments whose text is identical to the parameter name.", + "scope": "resource" + }, + "typescript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of parameter types.", + "scope": "resource" + }, + "typescript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of variable types.", + "scope": "resource" + }, + "typescript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of property declarations.", + "scope": "resource" + }, + "typescript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of return type for function signatures.", + "scope": "resource" + }, + "typescript.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of enum member values.", + "scope": "resource" + }, + "javascript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "Disable parameter name hints.", + "Enable parameter name hints only for literal arguments.", + "Enable parameter name hints for literal and non-literal arguments." + ], + "default": "none", + "description": "Enable/disable inlay hints of parameter names.", + "scope": "resource" + }, + "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "description": "Suppress parameter name hints on arguments whose text is identical to the parameter name.", + "scope": "resource" + }, + "javascript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of parameter types.", + "scope": "resource" + }, + "javascript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of variable types.", + "scope": "resource" + }, + "javascript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of property declarations.", + "scope": "resource" + }, + "javascript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of return type for function signatures.", + "scope": "resource" + }, + "javascript.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of enum member values.", "scope": "resource" }, "javascript.autoClosingTags": { diff --git a/src/server/features/fileConfigurationManager.ts b/src/server/features/fileConfigurationManager.ts index 2b8141a..1c5b94d 100644 --- a/src/server/features/fileConfigurationManager.ts +++ b/src/server/features/fileConfigurationManager.ts @@ -193,6 +193,7 @@ export default class FileConfigurationManager { includeCompletionsWithSnippetText: suggestConfig.includeCompletionsWithSnippetText, allowIncompleteCompletions: true, displayPartsForJSDoc: true, + ...getInlayHintsPreferences(language), } return preferences } @@ -240,3 +241,34 @@ function getJsxAttributeCompletionStyle(config: WorkspaceConfiguration) { default: return 'auto' } } + +export class InlayHintSettingNames { + static readonly parameterNamesSuppressWhenArgumentMatchesName = 'inlayHints.parameterNames.suppressWhenArgumentMatchesName' + static readonly parameterNamesEnabled = 'inlayHints.parameterTypes.enabled' + static readonly variableTypesEnabled = 'inlayHints.variableTypes.enabled' + static readonly propertyDeclarationTypesEnabled = 'inlayHints.propertyDeclarationTypes.enabled' + static readonly functionLikeReturnTypesEnabled = 'inlayHints.functionLikeReturnTypes.enabled' + static readonly enumMemberValuesEnabled = 'inlayHints.enumMemberValues.enabled' +} + +export function getInlayHintsPreferences(language: string) { + const config = workspace.getConfiguration(language) + return { + includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config), + includeInlayParameterNameHintsWhenArgumentMatchesName: !config.get(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true), + includeInlayFunctionParameterTypeHints: config.get(InlayHintSettingNames.parameterNamesEnabled, false), + includeInlayVariableTypeHints: config.get(InlayHintSettingNames.variableTypesEnabled, false), + includeInlayPropertyDeclarationTypeHints: config.get(InlayHintSettingNames.propertyDeclarationTypesEnabled, false), + includeInlayFunctionLikeReturnTypeHints: config.get(InlayHintSettingNames.functionLikeReturnTypesEnabled, false), + includeInlayEnumMemberValueHints: config.get(InlayHintSettingNames.enumMemberValuesEnabled, false), + } as const +} + +function getInlayParameterNameHintsPreference(config: WorkspaceConfiguration) { + switch (config.get('inlayHints.parameterNames.enabled')) { + case 'none': return 'none' + case 'literals': return 'literals' + case 'all': return 'all' + default: return undefined + } +} diff --git a/src/server/features/inlayHints.ts b/src/server/features/inlayHints.ts new file mode 100644 index 0000000..c018a89 --- /dev/null +++ b/src/server/features/inlayHints.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CancellationTokenSource, Disposable, disposeAll, Document, events, Position, Range, TextDocument, workspace } from 'coc.nvim' +import type * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import API from '../utils/api' +import FileConfigurationManager, { getInlayHintsPreferences } from './fileConfigurationManager' + +export enum InlayHintKind { + Other = 0, + Type = 1, + Parameter = 2 +} + +export interface InlayHint { + text: string + position: Position + kind: InlayHintKind + whitespaceBefore?: boolean + whitespaceAfter?: boolean +} + +export default class TypeScriptInlayHintsProvider implements Disposable { + public static readonly minVersion = API.v440 + private readonly inlayHintsNS = workspace.createNameSpace('tsserver-inlay-hint') + + private _disposables: Disposable[] = [] + private _tokenSource: CancellationTokenSource | undefined = undefined + private _inlayHints: Map = new Map() + + public dispose() { + if (this._tokenSource) { + this._tokenSource.cancel() + this._tokenSource.dispose() + this._tokenSource = undefined + } + + disposeAll(this._disposables) + this._disposables = [] + this._inlayHints.clear() + } + + constructor(private readonly client: ITypeScriptServiceClient, private readonly fileConfigurationManager: FileConfigurationManager) { + events.on('InsertLeave', async bufnr => { + const doc = workspace.getDocument(bufnr) + await this.syncAndRenderHints(doc) + }, this, this._disposables) + + workspace.onDidOpenTextDocument(async e => { + const doc = workspace.getDocument(e.bufnr) + await this.syncAndRenderHints(doc) + }, this, this._disposables) + + workspace.onDidChangeTextDocument(async e => { + const doc = workspace.getDocument(e.bufnr) + await this.syncAndRenderHints(doc) + }, this, this._disposables) + + this.syncAndRenderHints() + } + + private async syncAndRenderHints(doc?: Document) { + if (!doc) doc = await workspace.document + if (!isESDocument(doc)) return + + if (this._tokenSource) { + this._tokenSource.cancel() + this._tokenSource.dispose() + } + + try { + this._tokenSource = new CancellationTokenSource() + const { token } = this._tokenSource + const range = Range.create(0, 0, doc.lineCount, doc.getline(doc.lineCount).length) + const hints = await this.provideInlayHints(doc.textDocument, range, token) + if (token.isCancellationRequested) return + + await this.renderHints(doc, hints) + } catch (e) { + console.error(e) + this._tokenSource.cancel() + this._tokenSource.dispose() + } + } + + private async renderHints(doc: Document, hints: InlayHint[]) { + this._inlayHints.set(doc.uri, hints) + + const chaining_hints = {} + for (const item of hints) { + const chunks: [[string, string]] = [[item.text, 'CocHintSign']] + if (chaining_hints[item.position.line] === undefined) { + chaining_hints[item.position.line] = chunks + } else { + chaining_hints[item.position.line].push([' ', 'Normal']) + chaining_hints[item.position.line].push(chunks[0]) + } + } + + doc.buffer.clearNamespace(this.inlayHintsNS) + Object.keys(chaining_hints).forEach(async (line) => { + await doc.buffer.setVirtualText(this.inlayHintsNS, Number(line), chaining_hints[line], {}) + }) + } + + private inlayHintsEnabled(language: string) { + const preferences = getInlayHintsPreferences(language) + return preferences.includeInlayParameterNameHints === 'literals' + || preferences.includeInlayParameterNameHints === 'all' + || preferences.includeInlayEnumMemberValueHints + || preferences.includeInlayFunctionLikeReturnTypeHints + || preferences.includeInlayFunctionParameterTypeHints + || preferences.includeInlayPropertyDeclarationTypeHints + || preferences.includeInlayVariableTypeHints + } + + async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise { + if (!this.inlayHintsEnabled(document.languageId)) return [] + + const filepath = this.client.toOpenedFilePath(document.uri) + if (!filepath) return [] + + const start = document.offsetAt(range.start) + const length = document.offsetAt(range.end) - start + + await this.fileConfigurationManager.ensureConfigurationForDocument(document, token) + + const response = await this.client.execute('provideInlayHints', { file: filepath, start, length }, token) + if (response.type !== 'response' || !response.success || !response.body) { + return [] + } + + return response.body.map(hint => { + return { + text: hint.text, + position: Position.create(hint.position.line - 1, hint.position.offset - 1), + kind: hint.kind && fromProtocolInlayHintKind(hint.kind), + whitespaceAfter: hint.whitespaceAfter, + whitespaceBefore: hint.whitespaceBefore, + } + }) + } +} + +function isESDocument(doc: Document) { + if (!doc || !doc.attached) return false + return doc.filetype === 'typescript' || doc.filetype === 'javascript' +} + +function fromProtocolInlayHintKind(kind: Proto.InlayHintKind): InlayHintKind { + switch (kind) { + case 'Parameter': return InlayHintKind.Parameter + case 'Type': return InlayHintKind.Type + case 'Enum': return InlayHintKind.Other + default: return InlayHintKind.Other + } +} diff --git a/src/server/languageProvider.ts b/src/server/languageProvider.ts index c68d29f..9330fe0 100644 --- a/src/server/languageProvider.ts +++ b/src/server/languageProvider.ts @@ -29,6 +29,7 @@ import SignatureHelpProvider from './features/signatureHelp' import SemanticTokensProvider from './features/semanticTokens' import SmartSelection from './features/smartSelect' import TagClosing from './features/tagClosing' +import TypeScriptInlayHintsProvider from './features/inlayHints' import UpdateImportsOnFileRenameHandler from './features/updatePathOnRename' import { OrganizeImportsCodeActionProvider } from './organizeImports' import TypeScriptServiceClient from './typescriptServiceClient' @@ -159,6 +160,9 @@ export default class LanguageProvider { if (this.client.apiVersion.gte(API.v300)) { this._register(new TagClosing(this.client, this.description.id)) } + if (this.client.apiVersion.gte(API.v440) && workspace.isNvim) { + this._register(new TypeScriptInlayHintsProvider(this.client, this.fileConfigurationManager)) + } } public handles(resource: string, doc: TextDocument): boolean { diff --git a/src/server/typescriptService.ts b/src/server/typescriptService.ts index 2b6f7b6..88a097b 100644 --- a/src/server/typescriptService.ts +++ b/src/server/typescriptService.ts @@ -84,6 +84,7 @@ export interface TypeScriptRequestTypes { 'provideCallHierarchyIncomingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyIncomingCallsResponse] 'provideCallHierarchyOutgoingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyOutgoingCallsResponse] 'fileReferences': [Proto.FileRequestArgs, Proto.FileReferencesResponse] + 'provideInlayHints': [Proto.InlayHintsRequestArgs, Proto.InlayHintsResponse] } export interface ITypeScriptServiceClient { diff --git a/src/server/utils/api.ts b/src/server/utils/api.ts index 9e0dc5a..0dce70a 100644 --- a/src/server/utils/api.ts +++ b/src/server/utils/api.ts @@ -43,6 +43,7 @@ export default class API { public static readonly v401 = API.fromSimpleString('4.0.1') public static readonly v420 = API.fromSimpleString('4.2.0') public static readonly v430 = API.fromSimpleString('4.3.0') + public static readonly v440 = API.fromSimpleString('4.4.0') public static fromVersionString(versionString: string): API { let version = semver.valid(versionString)