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",
"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",

View file

@ -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<boolean>('paths', true),
completeFunctionCalls: config.get<boolean>('completeFunctionCalls', true),
autoImports: config.get<boolean>('autoImports', true),
// @ts-expect-error until 4.7
includeCompletionsWithObjectLiteralMethodSnippets: config.get<boolean>('suggest.objectLiteralMethodSnippets.enabled', true),
generateReturnInDocTemplate: config.get<boolean>('jsdoc.generateReturns', true),
importStatementSuggestions: config.get<boolean>('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<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,
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<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.
*--------------------------------------------------------------------------------------------*/
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<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()
}
private disposables: Disposable[] = []
private readonly _onDidChangeInlayHints = new Emitter<void>()
public readonly onDidChangeInlayHints: Event<void> = 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)
// 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)
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)
this.renderHintsForAllDocuments()
}, null, this.disposables)
}
private async renderHintsForAllDocuments(): Promise<void> {
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<InlayHint[]> {
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
}

View file

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

View file

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

View file

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

View file

@ -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',

View file

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