feat: inlay hints support (#335)

close #300
This commit is contained in:
Heyward Fann 2021-12-24 18:29:47 +08:00 committed by GitHub
parent 7b363e237e
commit de68137850
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 303 additions and 1 deletions

View file

@ -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": {

View file

@ -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<boolean>(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true),
includeInlayFunctionParameterTypeHints: config.get<boolean>(InlayHintSettingNames.parameterNamesEnabled, false),
includeInlayVariableTypeHints: config.get<boolean>(InlayHintSettingNames.variableTypesEnabled, false),
includeInlayPropertyDeclarationTypeHints: config.get<boolean>(InlayHintSettingNames.propertyDeclarationTypesEnabled, false),
includeInlayFunctionLikeReturnTypeHints: config.get<boolean>(InlayHintSettingNames.functionLikeReturnTypesEnabled, false),
includeInlayEnumMemberValueHints: config.get<boolean>(InlayHintSettingNames.enumMemberValuesEnabled, false),
} as const
}
function getInlayParameterNameHintsPreference(config: WorkspaceConfiguration) {
switch (config.get<string>('inlayHints.parameterNames.enabled')) {
case 'none': return 'none'
case 'literals': return 'literals'
case 'all': return 'all'
default: return undefined
}
}

View file

@ -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<string, InlayHint[]> = 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<InlayHint[]> {
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
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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)