rework inlayHints features

Use languages.registerInlayHintsProvider API
This commit is contained in:
Qiming Zhao 2022-05-06 15:05:06 +08:00
parent d27643a27c
commit 3a41bbe045
No known key found for this signature in database
GPG key ID: 9722CD0E8D4DCB8C
8 changed files with 81 additions and 152 deletions

View file

@ -913,7 +913,7 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^12.12.12", "@types/node": "^12.12.12",
"coc.nvim": "^0.0.81-next.23", "coc.nvim": "^0.0.81-next.25",
"esbuild": "^0.14.11", "esbuild": "^0.14.11",
"semver": "^7.3.5", "semver": "^7.3.5",
"vscode-languageserver-protocol": "^3.16.0", "vscode-languageserver-protocol": "^3.16.0",

View file

@ -42,6 +42,7 @@ export interface SuggestOptions {
readonly includeCompletionsWithSnippetText: boolean readonly includeCompletionsWithSnippetText: boolean
readonly includeCompletionsWithClassMemberSnippets: boolean readonly includeCompletionsWithClassMemberSnippets: boolean
readonly generateReturnInDocTemplate: boolean readonly generateReturnInDocTemplate: boolean
readonly includeCompletionsWithObjectLiteralMethodSnippets: boolean
} }
export default class FileConfigurationManager { export default class FileConfigurationManager {
@ -167,7 +168,6 @@ export default class FileConfigurationManager {
paths: config.get<boolean>('paths', true), paths: config.get<boolean>('paths', true),
completeFunctionCalls: config.get<boolean>('completeFunctionCalls', true), completeFunctionCalls: config.get<boolean>('completeFunctionCalls', true),
autoImports: config.get<boolean>('autoImports', true), autoImports: config.get<boolean>('autoImports', true),
// @ts-expect-error until 4.7
includeCompletionsWithObjectLiteralMethodSnippets: config.get<boolean>('suggest.objectLiteralMethodSnippets.enabled', true), includeCompletionsWithObjectLiteralMethodSnippets: config.get<boolean>('suggest.objectLiteralMethodSnippets.enabled', true),
generateReturnInDocTemplate: config.get<boolean>('jsdoc.generateReturns', true), generateReturnInDocTemplate: config.get<boolean>('jsdoc.generateReturns', true),
importStatementSuggestions: config.get<boolean>('importStatements', true), importStatementSuggestions: config.get<boolean>('importStatements', true),
@ -182,26 +182,31 @@ export default class FileConfigurationManager {
if (this.client.apiVersion.lt(API.v290)) { if (this.client.apiVersion.lt(API.v290)) {
return {} return {}
} }
const config = workspace.getConfiguration(`${language}.preferences`, uri) const config = workspace.getConfiguration(language, uri)
const preferencesConfig = workspace.getConfiguration(`${language}.preferences`, uri)
const suggestConfig = this.getCompleteOptions(language) const suggestConfig = this.getCompleteOptions(language)
// getImportModuleSpecifierEndingPreference available on ts 2.9.0 // getImportModuleSpecifierEndingPreference available on ts 2.9.0
const preferences: Proto.UserPreferences = { const preferences: Proto.UserPreferences = {
quotePreference: this.getQuoteStyle(config), quotePreference: this.getQuoteStyle(preferencesConfig),
importModuleSpecifierPreference: getImportModuleSpecifier(config) as any, importModuleSpecifierPreference: getImportModuleSpecifier(preferencesConfig) as any,
importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(config), importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(preferencesConfig),
jsxAttributeCompletionStyle: getJsxAttributeCompletionStyle(config), jsxAttributeCompletionStyle: getJsxAttributeCompletionStyle(preferencesConfig),
allowTextChangesInNewFiles: uri.startsWith('file:'), allowTextChangesInNewFiles: uri.startsWith('file:'),
allowRenameOfImportPath: true, allowRenameOfImportPath: true,
// can't support it with coc.nvim by now. // can't support it with coc.nvim by now.
provideRefactorNotApplicableReason: false, provideRefactorNotApplicableReason: false,
providePrefixAndSuffixTextForRename: config.get<boolean>('renameShorthandProperties', true) === false ? false : config.get<boolean>('useAliasesForRenames', true), providePrefixAndSuffixTextForRename: preferencesConfig.get<boolean>('renameShorthandProperties', true) === false ? false : preferencesConfig.get<boolean>('useAliasesForRenames', true),
generateReturnInDocTemplate: suggestConfig.generateReturnInDocTemplate, generateReturnInDocTemplate: suggestConfig.generateReturnInDocTemplate,
includeCompletionsForImportStatements: suggestConfig.includeCompletionsForImportStatements, includeCompletionsForImportStatements: suggestConfig.includeCompletionsForImportStatements,
includeCompletionsWithClassMemberSnippets: suggestConfig.includeCompletionsWithClassMemberSnippets, includeCompletionsWithClassMemberSnippets: suggestConfig.includeCompletionsWithClassMemberSnippets,
includeCompletionsWithSnippetText: suggestConfig.includeCompletionsWithSnippetText, includeCompletionsWithSnippetText: suggestConfig.includeCompletionsWithSnippetText,
// @ts-expect-error until 4.7
includeCompletionsWithObjectLiteralMethodSnippets: suggestConfig.includeCompletionsWithObjectLiteralMethodSnippets,
includeAutomaticOptionalChainCompletions: suggestConfig.includeAutomaticOptionalChainCompletions,
useLabelDetailsInCompletionEntries: true,
allowIncompleteCompletions: true, allowIncompleteCompletions: true,
displayPartsForJSDoc: true, displayPartsForJSDoc: true,
...getInlayHintsPreferences(language), ...getInlayHintsPreferences(config),
} }
return preferences return preferences
} }
@ -259,8 +264,7 @@ export class InlayHintSettingNames {
static readonly enumMemberValuesEnabled = 'inlayHints.enumMemberValues.enabled' static readonly enumMemberValuesEnabled = 'inlayHints.enumMemberValues.enabled'
} }
export function getInlayHintsPreferences(language: string) { export function getInlayHintsPreferences(config: WorkspaceConfiguration) {
const config = workspace.getConfiguration(language)
return { return {
includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config), includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config),
includeInlayParameterNameHintsWhenArgumentMatchesName: !config.get<boolean>(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true), includeInlayParameterNameHintsWhenArgumentMatchesName: !config.get<boolean>(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true),

View file

@ -3,149 +3,56 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { CancellationToken, CancellationTokenSource, Disposable, disposeAll, Document, Position, Range, TextDocument, workspace } from 'coc.nvim' import { CancellationToken, Disposable, disposeAll, Emitter, Event, InlayHint, InlayHintKind, InlayHintsProvider, Range, TextDocument, workspace } from 'coc.nvim'
import type * as Proto from '../protocol' import type * as Proto from '../protocol'
import { ITypeScriptServiceClient } from '../typescriptService' import { ITypeScriptServiceClient } from '../typescriptService'
import API from '../utils/api' import API from '../utils/api'
import { LanguageDescription } from '../utils/languageDescription'
import * as typeConverters from '../utils/typeConverters'
import FileConfigurationManager, { getInlayHintsPreferences } from './fileConfigurationManager' import FileConfigurationManager, { getInlayHintsPreferences } from './fileConfigurationManager'
export enum InlayHintKind { export default class TypeScriptInlayHintsProvider implements InlayHintsProvider {
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 public static readonly minVersion = API.v440
private readonly inlayHintsNS = workspace.createNameSpace('tsserver-inlay-hint') private disposables: Disposable[] = []
private readonly _onDidChangeInlayHints = new Emitter<void>()
private _disposables: Disposable[] = [] public readonly onDidChangeInlayHints: Event<void> = this._onDidChangeInlayHints.event
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( constructor(
private readonly language: LanguageDescription,
private readonly client: ITypeScriptServiceClient, private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager, private readonly fileConfigurationManager: FileConfigurationManager,
private readonly languageIds: string[]
) { ) {
let languageId = this.languageIds[0] let section = `${language.id}.inlayHints`
let section = `${languageId}.inlayHints`
workspace.onDidChangeConfiguration(async e => { workspace.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration(section)) { if (e.affectsConfiguration(section)) {
for (let doc of workspace.documents) { this._onDidChangeInlayHints.fire()
if (!this.inlayHintsEnabled(languageId)) {
doc.buffer.clearNamespace(this.inlayHintsNS)
} else {
await this.syncAndRenderHints(doc)
}
}
} }
}, null, this._disposables) }, null, this.disposables)
workspace.onDidOpenTextDocument(async e => { // When a JS/TS file changes, change inlay hints for all visible editors
const doc = workspace.getDocument(e.bufnr) // since changes in one file can effect the hints the others.
await this.syncAndRenderHints(doc) workspace.onDidChangeTextDocument(e => {
}, null, this._disposables) let doc = workspace.getDocument(e.textDocument.uri)
if (language.languageIds.includes(doc.languageId)) {
workspace.onDidChangeTextDocument(async e => { this._onDidChangeInlayHints.fire()
const doc = workspace.getDocument(e.bufnr)
if (this.languageIds.includes(doc.textDocument.languageId)) {
this.renderHintsForAllDocuments()
} }
}, null, this._disposables) }, null, this.disposables)
this.renderHintsForAllDocuments()
} }
private async renderHintsForAllDocuments(): Promise<void> { public dispose(): void {
for (let doc of workspace.documents) { this._onDidChangeInlayHints.dispose()
await this.syncAndRenderHints(doc) disposeAll(this.disposables)
}
}
private async syncAndRenderHints(doc: Document) {
if (!this.languageIds.includes(doc.textDocument.languageId)) return
if (!this.inlayHintsEnabled(this.languageIds[0])) 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[]> { async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise<InlayHint[]> {
const filepath = this.client.toOpenedFilePath(document.uri) const filepath = this.client.toOpenedFilePath(document.uri)
if (!filepath) return [] if (!filepath) return []
if (!areInlayHintsEnabledForFile(this.language, document)) {
return []
}
const start = document.offsetAt(range.start) const start = document.offsetAt(range.start)
const length = document.offsetAt(range.end) - start const length = document.offsetAt(range.end) - start
await this.fileConfigurationManager.ensureConfigurationForDocument(document, token) await this.fileConfigurationManager.ensureConfigurationForDocument(document, token)
const response = await this.client.execute('provideInlayHints', { file: filepath, start, length }, token) const response = await this.client.execute('provideInlayHints', { file: filepath, start, length }, token)
if (response.type !== 'response' || !response.success || !response.body) { if (response.type !== 'response' || !response.success || !response.body) {
return [] return []
@ -153,11 +60,11 @@ export default class TypeScriptInlayHintsProvider implements Disposable {
return response.body.map(hint => { return response.body.map(hint => {
return { return {
text: hint.text, label: hint.text,
position: Position.create(hint.position.line - 1, hint.position.offset - 1), position: typeConverters.Position.fromLocation(hint.position),
kind: hint.kind && fromProtocolInlayHintKind(hint.kind), kind: fromProtocolInlayHintKind(hint.kind),
whitespaceAfter: hint.whitespaceAfter, paddingLeft: hint.whitespaceBefore,
whitespaceBefore: hint.whitespaceBefore, paddingRight: hint.whitespaceAfter,
} }
}) })
} }
@ -165,9 +72,21 @@ export default class TypeScriptInlayHintsProvider implements Disposable {
function fromProtocolInlayHintKind(kind: Proto.InlayHintKind): InlayHintKind { function fromProtocolInlayHintKind(kind: Proto.InlayHintKind): InlayHintKind {
switch (kind) { switch (kind) {
case 'Parameter': return InlayHintKind.Parameter case 'Parameter': return 2
case 'Type': return InlayHintKind.Type case 'Type': return 1
case 'Enum': return InlayHintKind.Other case 'Enum': return undefined
default: return InlayHintKind.Other default: return undefined
} }
} }
function areInlayHintsEnabledForFile(language: LanguageDescription, document: TextDocument) {
const config = workspace.getConfiguration(language.id, document.uri)
const preferences = getInlayHintsPreferences(config)
return preferences.includeInlayParameterNameHints === 'literals' ||
preferences.includeInlayParameterNameHints === 'all' ||
preferences.includeInlayEnumMemberValueHints ||
preferences.includeInlayFunctionLikeReturnTypeHints ||
preferences.includeInlayFunctionParameterTypeHints ||
preferences.includeInlayPropertyDeclarationTypeHints ||
preferences.includeInlayVariableTypeHints
}

View file

@ -41,7 +41,7 @@ export default class TsserverService implements IServiceProvider {
} }
}) })
this.selector = this.descriptions.reduce((arr, c) => { this.selector = this.descriptions.reduce((arr, c) => {
return arr.concat(c.modeIds) return arr.concat(c.languageIds)
}, []) }, [])
this.registCommands() this.registCommands()
} }

View file

@ -73,7 +73,7 @@ export default class LanguageProvider {
client: TypeScriptServiceClient, client: TypeScriptServiceClient,
typingsStatus: TypingsStatus typingsStatus: TypingsStatus
): void { ): void {
let languageIds = this.description.modeIds let languageIds = this.description.languageIds
let clientId = `tsc-${this.description.id}` let clientId = `tsc-${this.description.id}`
this._register( this._register(
languages.registerCompletionItemProvider(clientId, 'TSC', languageIds, languages.registerCompletionItemProvider(clientId, 'TSC', languageIds,
@ -167,13 +167,19 @@ export default class LanguageProvider {
if (this.client.apiVersion.gte(API.v300)) { if (this.client.apiVersion.gte(API.v300)) {
this._register(new TagClosing(this.client, this.description.id)) this._register(new TagClosing(this.client, this.description.id))
} }
if (this.client.apiVersion.gte(API.v440) && workspace.isNvim) { if (this.client.apiVersion.gte(API.v440)) {
this._register(new TypeScriptInlayHintsProvider(this.client, this.fileConfigurationManager, languageIds)) if (typeof languages.registerInlayHintsProvider === 'function') {
let provider = new TypeScriptInlayHintsProvider(this.description, this.client, this.fileConfigurationManager)
this._register(provider)
this._register(languages.registerInlayHintsProvider(languageIds, provider))
} else {
this.client.logger.error(`languages.registerInlayHintsProvider is not a function, inlay hints won't work`)
}
} }
} }
public handles(resource: string, doc: TextDocument): boolean { public handles(resource: string, doc: TextDocument): boolean {
if (doc && this.description.modeIds.includes(doc.languageId)) { if (doc && this.description.languageIds.includes(doc.languageId)) {
return true return true
} }
return this.handlesConfigFile(Uri.parse(resource)) return this.handlesConfigFile(Uri.parse(resource))

View file

@ -110,7 +110,7 @@ export default class TypeScriptServiceClientHost implements Disposable {
if (plugin.configNamespace && plugin.languages.length) { if (plugin.configNamespace && plugin.languages.length) {
this.registerExtensionLanguageProvider({ this.registerExtensionLanguageProvider({
id: plugin.configNamespace, id: plugin.configNamespace,
modeIds: Array.from(plugin.languages), languageIds: Array.from(plugin.languages),
diagnosticSource: 'ts-plugin', diagnosticSource: 'ts-plugin',
diagnosticLanguage: DiagnosticLanguage.TypeScript, diagnosticLanguage: DiagnosticLanguage.TypeScript,
diagnosticOwner: 'typescript', diagnosticOwner: 'typescript',
@ -127,7 +127,7 @@ export default class TypeScriptServiceClientHost implements Disposable {
if (languageIds.size) { if (languageIds.size) {
this.registerExtensionLanguageProvider({ this.registerExtensionLanguageProvider({
id: 'typescript-plugins', id: 'typescript-plugins',
modeIds: Array.from(languageIds.values()), languageIds: Array.from(languageIds.values()),
diagnosticSource: 'ts-plugin', diagnosticSource: 'ts-plugin',
diagnosticLanguage: DiagnosticLanguage.TypeScript, diagnosticLanguage: DiagnosticLanguage.TypeScript,
diagnosticOwner: 'typescript', diagnosticOwner: 'typescript',
@ -293,7 +293,7 @@ export default class TypeScriptServiceClientHost implements Disposable {
private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager): string[] { private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager): string[] {
const allModeIds = flatten([ const allModeIds = flatten([
...descriptions.map(x => x.modeIds), ...descriptions.map(x => x.languageIds),
...pluginManager.plugins.map(x => x.languages) ...pluginManager.plugins.map(x => x.languages)
]) ])
return allModeIds return allModeIds

View file

@ -10,7 +10,7 @@ export interface LanguageDescription {
readonly id: string readonly id: string
readonly diagnosticSource: string readonly diagnosticSource: string
readonly diagnosticLanguage: DiagnosticLanguage readonly diagnosticLanguage: DiagnosticLanguage
readonly modeIds: string[] readonly languageIds: string[]
readonly isExternal?: boolean readonly isExternal?: boolean
readonly diagnosticOwner: string readonly diagnosticOwner: string
readonly configFilePattern?: RegExp readonly configFilePattern?: RegExp
@ -28,7 +28,7 @@ export const standardLanguageDescriptions: LanguageDescription[] = [
diagnosticSource: 'ts', diagnosticSource: 'ts',
diagnosticOwner: 'typescript', diagnosticOwner: 'typescript',
diagnosticLanguage: DiagnosticLanguage.TypeScript, diagnosticLanguage: DiagnosticLanguage.TypeScript,
modeIds: [languageModeIds.typescript, languageModeIds.typescriptreact, languageModeIds.typescripttsx, languageModeIds.typescriptjsx], languageIds: [languageModeIds.typescript, languageModeIds.typescriptreact, languageModeIds.typescripttsx, languageModeIds.typescriptjsx],
configFilePattern: /^tsconfig(\..*)?\.json$/gi, configFilePattern: /^tsconfig(\..*)?\.json$/gi,
standardFileExtensions: [ standardFileExtensions: [
'ts', 'ts',
@ -41,7 +41,7 @@ export const standardLanguageDescriptions: LanguageDescription[] = [
id: 'javascript', id: 'javascript',
diagnosticSource: 'ts', diagnosticSource: 'ts',
diagnosticOwner: 'typescript', diagnosticOwner: 'typescript',
modeIds: [languageModeIds.javascript, languageModeIds.javascriptreact, languageModeIds.javascriptjsx], diagnosticLanguage: DiagnosticLanguage.JavaScript, languageIds: [languageModeIds.javascript, languageModeIds.javascriptreact, languageModeIds.javascriptjsx], diagnosticLanguage: DiagnosticLanguage.JavaScript,
configFilePattern: /^jsconfig(\..*)?\.json$/gi, configFilePattern: /^jsconfig(\..*)?\.json$/gi,
standardFileExtensions: [ standardFileExtensions: [
'js', 'js',

View file

@ -7,10 +7,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.41.tgz#81d7734c5257da9f04354bd9084a6ebbdd5198a5" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.41.tgz#81d7734c5257da9f04354bd9084a6ebbdd5198a5"
integrity sha512-f6xOqucbDirG7LOzedpvzjP3UTmHttRou3Mosx3vL9wr9AIQGhcPgVnqa8ihpZYnxyM1rxeNCvTyukPKZtq10Q== integrity sha512-f6xOqucbDirG7LOzedpvzjP3UTmHttRou3Mosx3vL9wr9AIQGhcPgVnqa8ihpZYnxyM1rxeNCvTyukPKZtq10Q==
coc.nvim@^0.0.81-next.23: coc.nvim@^0.0.81-next.25:
version "0.0.81-next.23" version "0.0.81-next.25"
resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.81-next.23.tgz#48d8238afaaa0738c6237d8c077ba1791e2f90c6" resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.81-next.25.tgz#8f84b7c71b742e111d330fb553b0df604d4929ec"
integrity sha512-RxNx6iRz7UvdgDeMVLyNXP6xe8GU8aY0Qxl/sBI+m2RzAJDRKCPuyFU1uOSxZntLglWtzKd87k3Byymdm19uBQ== integrity sha512-c0OOZQSjgKLGNhIpKzlxkPiPmMCmYHSVcCDNA26BqFX8X0iWt3xXqwbxKiE54zfIsz0wFqL59iBVGUSBaqHGpA==
esbuild-android-arm64@0.14.11: esbuild-android-arm64@0.14.11:
version "0.14.11" version "0.14.11"