rework inlayHints features
Use languages.registerInlayHintsProvider API
This commit is contained in:
parent
d27643a27c
commit
3a41bbe045
8 changed files with 81 additions and 152 deletions
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue