|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | +*--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import { CancellationToken, CancellationTokenSource, Disposable, disposeAll, Document, events, Position, Range, TextDocument, workspace } from 'coc.nvim' |
| 7 | +import type * as Proto from '../protocol' |
| 8 | +import { ITypeScriptServiceClient } from '../typescriptService' |
| 9 | +import API from '../utils/api' |
| 10 | +import FileConfigurationManager, { getInlayHintsPreferences } from './fileConfigurationManager' |
| 11 | + |
| 12 | +export enum InlayHintKind { |
| 13 | + Other = 0, |
| 14 | + Type = 1, |
| 15 | + Parameter = 2 |
| 16 | +} |
| 17 | + |
| 18 | +export interface InlayHint { |
| 19 | + text: string |
| 20 | + position: Position |
| 21 | + kind: InlayHintKind |
| 22 | + whitespaceBefore?: boolean |
| 23 | + whitespaceAfter?: boolean |
| 24 | +} |
| 25 | + |
| 26 | +export default class TypeScriptInlayHintsProvider implements Disposable { |
| 27 | + public static readonly minVersion = API.v440 |
| 28 | + private readonly inlayHintsNS = workspace.createNameSpace('tsserver-inlay-hint') |
| 29 | + |
| 30 | + private _disposables: Disposable[] = [] |
| 31 | + private _tokenSource: CancellationTokenSource | undefined = undefined |
| 32 | + private _inlayHints: Map<string, InlayHint[]> = new Map() |
| 33 | + |
| 34 | + public dispose() { |
| 35 | + if (this._tokenSource) { |
| 36 | + this._tokenSource.cancel() |
| 37 | + this._tokenSource.dispose() |
| 38 | + this._tokenSource = undefined |
| 39 | + } |
| 40 | + |
| 41 | + disposeAll(this._disposables) |
| 42 | + this._disposables = [] |
| 43 | + this._inlayHints.clear() |
| 44 | + } |
| 45 | + |
| 46 | + constructor(private readonly client: ITypeScriptServiceClient, private readonly fileConfigurationManager: FileConfigurationManager) { |
| 47 | + events.on('InsertLeave', async bufnr => { |
| 48 | + const doc = workspace.getDocument(bufnr) |
| 49 | + await this.syncAndRenderHints(doc) |
| 50 | + }, this, this._disposables) |
| 51 | + |
| 52 | + workspace.onDidOpenTextDocument(async e => { |
| 53 | + const doc = workspace.getDocument(e.bufnr) |
| 54 | + await this.syncAndRenderHints(doc) |
| 55 | + }, this, this._disposables) |
| 56 | + |
| 57 | + workspace.onDidChangeTextDocument(async e => { |
| 58 | + const doc = workspace.getDocument(e.bufnr) |
| 59 | + await this.syncAndRenderHints(doc) |
| 60 | + }, this, this._disposables) |
| 61 | + |
| 62 | + this.syncAndRenderHints() |
| 63 | + } |
| 64 | + |
| 65 | + private async syncAndRenderHints(doc?: Document) { |
| 66 | + if (!doc) doc = await workspace.document |
| 67 | + if (!isESDocument(doc)) return |
| 68 | + |
| 69 | + if (this._tokenSource) { |
| 70 | + this._tokenSource.cancel() |
| 71 | + this._tokenSource.dispose() |
| 72 | + } |
| 73 | + |
| 74 | + try { |
| 75 | + this._tokenSource = new CancellationTokenSource() |
| 76 | + const { token } = this._tokenSource |
| 77 | + const range = Range.create(0, 0, doc.lineCount, doc.getline(doc.lineCount).length) |
| 78 | + const hints = await this.provideInlayHints(doc.textDocument, range, token) |
| 79 | + if (token.isCancellationRequested) return |
| 80 | + |
| 81 | + await this.renderHints(doc, hints) |
| 82 | + } catch (e) { |
| 83 | + console.error(e) |
| 84 | + this._tokenSource.cancel() |
| 85 | + this._tokenSource.dispose() |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + private async renderHints(doc: Document, hints: InlayHint[]) { |
| 90 | + this._inlayHints.set(doc.uri, hints) |
| 91 | + |
| 92 | + const chaining_hints = {} |
| 93 | + for (const item of hints) { |
| 94 | + const chunks: [[string, string]] = [[item.text, 'CocHintSign']] |
| 95 | + if (chaining_hints[item.position.line] === undefined) { |
| 96 | + chaining_hints[item.position.line] = chunks |
| 97 | + } else { |
| 98 | + chaining_hints[item.position.line].push([' ', 'Normal']) |
| 99 | + chaining_hints[item.position.line].push(chunks[0]) |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + doc.buffer.clearNamespace(this.inlayHintsNS) |
| 104 | + Object.keys(chaining_hints).forEach(async (line) => { |
| 105 | + await doc.buffer.setVirtualText(this.inlayHintsNS, Number(line), chaining_hints[line], {}) |
| 106 | + }) |
| 107 | + } |
| 108 | + |
| 109 | + private inlayHintsEnabled(language: string) { |
| 110 | + const preferences = getInlayHintsPreferences(language) |
| 111 | + return preferences.includeInlayParameterNameHints === 'literals' |
| 112 | + || preferences.includeInlayParameterNameHints === 'all' |
| 113 | + || preferences.includeInlayEnumMemberValueHints |
| 114 | + || preferences.includeInlayFunctionLikeReturnTypeHints |
| 115 | + || preferences.includeInlayFunctionParameterTypeHints |
| 116 | + || preferences.includeInlayPropertyDeclarationTypeHints |
| 117 | + || preferences.includeInlayVariableTypeHints |
| 118 | + } |
| 119 | + |
| 120 | + async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise<InlayHint[]> { |
| 121 | + if (!this.inlayHintsEnabled(document.languageId)) return [] |
| 122 | + |
| 123 | + const filepath = this.client.toOpenedFilePath(document.uri) |
| 124 | + if (!filepath) return [] |
| 125 | + |
| 126 | + const start = document.offsetAt(range.start) |
| 127 | + const length = document.offsetAt(range.end) - start |
| 128 | + |
| 129 | + await this.fileConfigurationManager.ensureConfigurationForDocument(document, token) |
| 130 | + |
| 131 | + const response = await this.client.execute('provideInlayHints', { file: filepath, start, length }, token) |
| 132 | + if (response.type !== 'response' || !response.success || !response.body) { |
| 133 | + return [] |
| 134 | + } |
| 135 | + |
| 136 | + return response.body.map(hint => { |
| 137 | + return { |
| 138 | + text: hint.text, |
| 139 | + position: Position.create(hint.position.line - 1, hint.position.offset - 1), |
| 140 | + kind: hint.kind && fromProtocolInlayHintKind(hint.kind), |
| 141 | + whitespaceAfter: hint.whitespaceAfter, |
| 142 | + whitespaceBefore: hint.whitespaceBefore, |
| 143 | + } |
| 144 | + }) |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +function isESDocument(doc: Document) { |
| 149 | + if (!doc || !doc.attached) return false |
| 150 | + return doc.filetype === 'typescript' || doc.filetype === 'javascript' |
| 151 | +} |
| 152 | + |
| 153 | +function fromProtocolInlayHintKind(kind: Proto.InlayHintKind): InlayHintKind { |
| 154 | + switch (kind) { |
| 155 | + case 'Parameter': return InlayHintKind.Parameter |
| 156 | + case 'Type': return InlayHintKind.Type |
| 157 | + case 'Enum': return InlayHintKind.Other |
| 158 | + default: return InlayHintKind.Other |
| 159 | + } |
| 160 | +} |
0 commit comments