From 3a41bbe0450fcad769e75d3988c13b7dc56c2032 Mon Sep 17 00:00:00 2001 From: Qiming Zhao Date: Fri, 6 May 2022 15:05:06 +0800 Subject: [PATCH] rework inlayHints features Use languages.registerInlayHintsProvider API --- package.json | 2 +- .../features/fileConfigurationManager.ts | 24 ++- src/server/features/inlayHints.ts | 171 +++++------------- src/server/index.ts | 2 +- src/server/languageProvider.ts | 14 +- src/server/typescriptServiceClientHost.ts | 6 +- src/server/utils/languageDescription.ts | 6 +- yarn.lock | 8 +- 8 files changed, 81 insertions(+), 152 deletions(-) diff --git a/package.json b/package.json index 484ecc5..2ca9468 100644 --- a/package.json +++ b/package.json @@ -913,7 +913,7 @@ "license": "MIT", "devDependencies": { "@types/node": "^12.12.12", - "coc.nvim": "^0.0.81-next.23", + "coc.nvim": "^0.0.81-next.25", "esbuild": "^0.14.11", "semver": "^7.3.5", "vscode-languageserver-protocol": "^3.16.0", diff --git a/src/server/features/fileConfigurationManager.ts b/src/server/features/fileConfigurationManager.ts index 49184c0..f6674a6 100644 --- a/src/server/features/fileConfigurationManager.ts +++ b/src/server/features/fileConfigurationManager.ts @@ -42,6 +42,7 @@ export interface SuggestOptions { readonly includeCompletionsWithSnippetText: boolean readonly includeCompletionsWithClassMemberSnippets: boolean readonly generateReturnInDocTemplate: boolean + readonly includeCompletionsWithObjectLiteralMethodSnippets: boolean } export default class FileConfigurationManager { @@ -167,7 +168,6 @@ export default class FileConfigurationManager { paths: config.get('paths', true), completeFunctionCalls: config.get('completeFunctionCalls', true), autoImports: config.get('autoImports', true), - // @ts-expect-error until 4.7 includeCompletionsWithObjectLiteralMethodSnippets: config.get('suggest.objectLiteralMethodSnippets.enabled', true), generateReturnInDocTemplate: config.get('jsdoc.generateReturns', true), importStatementSuggestions: config.get('importStatements', true), @@ -182,26 +182,31 @@ export default class FileConfigurationManager { if (this.client.apiVersion.lt(API.v290)) { 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) // getImportModuleSpecifierEndingPreference available on ts 2.9.0 const preferences: Proto.UserPreferences = { - quotePreference: this.getQuoteStyle(config), - importModuleSpecifierPreference: getImportModuleSpecifier(config) as any, - importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(config), - jsxAttributeCompletionStyle: getJsxAttributeCompletionStyle(config), + quotePreference: this.getQuoteStyle(preferencesConfig), + importModuleSpecifierPreference: getImportModuleSpecifier(preferencesConfig) as any, + importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(preferencesConfig), + jsxAttributeCompletionStyle: getJsxAttributeCompletionStyle(preferencesConfig), allowTextChangesInNewFiles: uri.startsWith('file:'), allowRenameOfImportPath: true, // can't support it with coc.nvim by now. provideRefactorNotApplicableReason: false, - providePrefixAndSuffixTextForRename: config.get('renameShorthandProperties', true) === false ? false : config.get('useAliasesForRenames', true), + providePrefixAndSuffixTextForRename: preferencesConfig.get('renameShorthandProperties', true) === false ? false : preferencesConfig.get('useAliasesForRenames', true), generateReturnInDocTemplate: suggestConfig.generateReturnInDocTemplate, includeCompletionsForImportStatements: suggestConfig.includeCompletionsForImportStatements, includeCompletionsWithClassMemberSnippets: suggestConfig.includeCompletionsWithClassMemberSnippets, includeCompletionsWithSnippetText: suggestConfig.includeCompletionsWithSnippetText, + // @ts-expect-error until 4.7 + includeCompletionsWithObjectLiteralMethodSnippets: suggestConfig.includeCompletionsWithObjectLiteralMethodSnippets, + includeAutomaticOptionalChainCompletions: suggestConfig.includeAutomaticOptionalChainCompletions, + useLabelDetailsInCompletionEntries: true, allowIncompleteCompletions: true, displayPartsForJSDoc: true, - ...getInlayHintsPreferences(language), + ...getInlayHintsPreferences(config), } return preferences } @@ -259,8 +264,7 @@ export class InlayHintSettingNames { static readonly enumMemberValuesEnabled = 'inlayHints.enumMemberValues.enabled' } -export function getInlayHintsPreferences(language: string) { - const config = workspace.getConfiguration(language) +export function getInlayHintsPreferences(config: WorkspaceConfiguration) { return { includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config), includeInlayParameterNameHintsWhenArgumentMatchesName: !config.get(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true), diff --git a/src/server/features/inlayHints.ts b/src/server/features/inlayHints.ts index 30ad69a..4ab268e 100644 --- a/src/server/features/inlayHints.ts +++ b/src/server/features/inlayHints.ts @@ -3,149 +3,56 @@ * 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 { ITypeScriptServiceClient } from '../typescriptService' import API from '../utils/api' +import { LanguageDescription } from '../utils/languageDescription' +import * as typeConverters from '../utils/typeConverters' 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 { +export default class TypeScriptInlayHintsProvider implements InlayHintsProvider { 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() - } + private disposables: Disposable[] = [] + private readonly _onDidChangeInlayHints = new Emitter() + public readonly onDidChangeInlayHints: Event = this._onDidChangeInlayHints.event constructor( + private readonly language: LanguageDescription, private readonly client: ITypeScriptServiceClient, private readonly fileConfigurationManager: FileConfigurationManager, - private readonly languageIds: string[] ) { - let languageId = this.languageIds[0] - let section = `${languageId}.inlayHints` + let section = `${language.id}.inlayHints` workspace.onDidChangeConfiguration(async e => { if (e.affectsConfiguration(section)) { - for (let doc of workspace.documents) { - if (!this.inlayHintsEnabled(languageId)) { - doc.buffer.clearNamespace(this.inlayHintsNS) - } else { - await this.syncAndRenderHints(doc) - } - } + this._onDidChangeInlayHints.fire() } - }, null, this._disposables) - workspace.onDidOpenTextDocument(async e => { - const doc = workspace.getDocument(e.bufnr) - await this.syncAndRenderHints(doc) - }, null, this._disposables) - - workspace.onDidChangeTextDocument(async e => { - const doc = workspace.getDocument(e.bufnr) - if (this.languageIds.includes(doc.textDocument.languageId)) { - this.renderHintsForAllDocuments() + }, null, this.disposables) + // When a JS/TS file changes, change inlay hints for all visible editors + // since changes in one file can effect the hints the others. + workspace.onDidChangeTextDocument(e => { + let doc = workspace.getDocument(e.textDocument.uri) + if (language.languageIds.includes(doc.languageId)) { + this._onDidChangeInlayHints.fire() } - }, null, this._disposables) - - this.renderHintsForAllDocuments() + }, null, this.disposables) } - private async renderHintsForAllDocuments(): Promise { - for (let doc of workspace.documents) { - await this.syncAndRenderHints(doc) - } - } - - 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 + public dispose(): void { + this._onDidChangeInlayHints.dispose() + disposeAll(this.disposables) } async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise { const filepath = this.client.toOpenedFilePath(document.uri) if (!filepath) return [] + if (!areInlayHintsEnabledForFile(this.language, document)) { + 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 [] @@ -153,11 +60,11 @@ export default class TypeScriptInlayHintsProvider implements Disposable { 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, + label: hint.text, + position: typeConverters.Position.fromLocation(hint.position), + kind: fromProtocolInlayHintKind(hint.kind), + paddingLeft: hint.whitespaceBefore, + paddingRight: hint.whitespaceAfter, } }) } @@ -165,9 +72,21 @@ export default class TypeScriptInlayHintsProvider implements Disposable { 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 + case 'Parameter': return 2 + case 'Type': return 1 + case 'Enum': return undefined + 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 +} diff --git a/src/server/index.ts b/src/server/index.ts index d1866f6..2d42456 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -41,7 +41,7 @@ export default class TsserverService implements IServiceProvider { } }) this.selector = this.descriptions.reduce((arr, c) => { - return arr.concat(c.modeIds) + return arr.concat(c.languageIds) }, []) this.registCommands() } diff --git a/src/server/languageProvider.ts b/src/server/languageProvider.ts index 4a58133..7560ad2 100644 --- a/src/server/languageProvider.ts +++ b/src/server/languageProvider.ts @@ -73,7 +73,7 @@ export default class LanguageProvider { client: TypeScriptServiceClient, typingsStatus: TypingsStatus ): void { - let languageIds = this.description.modeIds + let languageIds = this.description.languageIds let clientId = `tsc-${this.description.id}` this._register( languages.registerCompletionItemProvider(clientId, 'TSC', languageIds, @@ -167,13 +167,19 @@ 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, languageIds)) + if (this.client.apiVersion.gte(API.v440)) { + 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 { - if (doc && this.description.modeIds.includes(doc.languageId)) { + if (doc && this.description.languageIds.includes(doc.languageId)) { return true } return this.handlesConfigFile(Uri.parse(resource)) diff --git a/src/server/typescriptServiceClientHost.ts b/src/server/typescriptServiceClientHost.ts index dd44d49..43e591f 100644 --- a/src/server/typescriptServiceClientHost.ts +++ b/src/server/typescriptServiceClientHost.ts @@ -110,7 +110,7 @@ export default class TypeScriptServiceClientHost implements Disposable { if (plugin.configNamespace && plugin.languages.length) { this.registerExtensionLanguageProvider({ id: plugin.configNamespace, - modeIds: Array.from(plugin.languages), + languageIds: Array.from(plugin.languages), diagnosticSource: 'ts-plugin', diagnosticLanguage: DiagnosticLanguage.TypeScript, diagnosticOwner: 'typescript', @@ -127,7 +127,7 @@ export default class TypeScriptServiceClientHost implements Disposable { if (languageIds.size) { this.registerExtensionLanguageProvider({ id: 'typescript-plugins', - modeIds: Array.from(languageIds.values()), + languageIds: Array.from(languageIds.values()), diagnosticSource: 'ts-plugin', diagnosticLanguage: DiagnosticLanguage.TypeScript, diagnosticOwner: 'typescript', @@ -293,7 +293,7 @@ export default class TypeScriptServiceClientHost implements Disposable { private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager): string[] { const allModeIds = flatten([ - ...descriptions.map(x => x.modeIds), + ...descriptions.map(x => x.languageIds), ...pluginManager.plugins.map(x => x.languages) ]) return allModeIds diff --git a/src/server/utils/languageDescription.ts b/src/server/utils/languageDescription.ts index a796a9a..dfa6c7e 100644 --- a/src/server/utils/languageDescription.ts +++ b/src/server/utils/languageDescription.ts @@ -10,7 +10,7 @@ export interface LanguageDescription { readonly id: string readonly diagnosticSource: string readonly diagnosticLanguage: DiagnosticLanguage - readonly modeIds: string[] + readonly languageIds: string[] readonly isExternal?: boolean readonly diagnosticOwner: string readonly configFilePattern?: RegExp @@ -28,7 +28,7 @@ export const standardLanguageDescriptions: LanguageDescription[] = [ diagnosticSource: 'ts', diagnosticOwner: '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, standardFileExtensions: [ 'ts', @@ -41,7 +41,7 @@ export const standardLanguageDescriptions: LanguageDescription[] = [ id: 'javascript', diagnosticSource: 'ts', 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, standardFileExtensions: [ 'js', diff --git a/yarn.lock b/yarn.lock index a37f5dc..9f95a87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.41.tgz#81d7734c5257da9f04354bd9084a6ebbdd5198a5" integrity sha512-f6xOqucbDirG7LOzedpvzjP3UTmHttRou3Mosx3vL9wr9AIQGhcPgVnqa8ihpZYnxyM1rxeNCvTyukPKZtq10Q== -coc.nvim@^0.0.81-next.23: - version "0.0.81-next.23" - resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.81-next.23.tgz#48d8238afaaa0738c6237d8c077ba1791e2f90c6" - integrity sha512-RxNx6iRz7UvdgDeMVLyNXP6xe8GU8aY0Qxl/sBI+m2RzAJDRKCPuyFU1uOSxZntLglWtzKd87k3Byymdm19uBQ== +coc.nvim@^0.0.81-next.25: + version "0.0.81-next.25" + resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.81-next.25.tgz#8f84b7c71b742e111d330fb553b0df604d4929ec" + integrity sha512-c0OOZQSjgKLGNhIpKzlxkPiPmMCmYHSVcCDNA26BqFX8X0iWt3xXqwbxKiE54zfIsz0wFqL59iBVGUSBaqHGpA== esbuild-android-arm64@0.14.11: version "0.14.11"