1
0
Fork 0
mirror of synced 2024-06-25 18:21:11 -04:00
ultimate-vim/sources_non_forked/coc.nvim/src/handler/signature.ts
2022-07-20 13:20:15 +08:00

278 lines
10 KiB
TypeScript

'use strict'
import { Neovim } from '@chemzqm/neovim'
import { CancellationTokenSource, Disposable, MarkupContent, Position, SignatureHelp, SignatureHelpTriggerKind } from 'vscode-languageserver-protocol'
import events from '../events'
import languages from '../languages'
import Document from '../model/document'
import FloatFactory from '../model/floatFactory'
import { ConfigurationChangeEvent, FloatConfig, HandlerDelegate } from '../types'
import { disposeAll, isMarkdown } from '../util'
import { byteLength } from '../util/string'
import workspace from '../workspace'
const logger = require('../util/logger')('handler-signature')
interface SignatureConfig {
wait: number
trigger: boolean
target: string
preferAbove: boolean
hideOnChange: boolean
floatConfig: FloatConfig
}
interface SignaturePosition {
bufnr: number
lnum: number
col: number
}
interface SignaturePart {
text: string
type: 'Label' | 'MoreMsg' | 'Normal'
}
export default class Signature {
private timer: NodeJS.Timer
private config: SignatureConfig
private signatureFactory: FloatFactory
private lastPosition: SignaturePosition | undefined
private disposables: Disposable[] = []
private tokenSource: CancellationTokenSource | undefined
constructor(private nvim: Neovim, private handler: HandlerDelegate) {
this.signatureFactory = new FloatFactory(nvim)
this.loadConfiguration()
this.disposables.push(this.signatureFactory)
workspace.onDidChangeConfiguration(this.loadConfiguration, this, this.disposables)
events.on('CursorMovedI', async (bufnr, cursor) => {
let pos = this.lastPosition
if (!pos) return
// avoid close signature for valid position.
if (pos.bufnr == bufnr && pos.lnum == cursor[0] && pos.col <= cursor[1]) return
this.signatureFactory.close()
}, null, this.disposables)
events.on(['InsertLeave', 'BufEnter'], () => {
this.tokenSource?.cancel()
}, null, this.disposables)
events.on('TextChangedI', () => {
if (this.config.hideOnChange) {
this.signatureFactory.close()
}
}, null, this.disposables)
events.on('TextInsert', async (bufnr, info, character) => {
if (!this.config.trigger) return
let doc = this.getTextDocument(bufnr)
if (!doc || !languages.shouldTriggerSignatureHelp(doc.textDocument, character)) return
await this._triggerSignatureHelp(doc, { line: info.lnum - 1, character: info.pre.length }, false)
}, null, this.disposables)
}
private getTextDocument(bufnr: number): Document | undefined {
let doc = workspace.getDocument(bufnr)
if (!doc || doc.isCommandLine || !doc.attached) return
return doc
}
private loadConfiguration(e?: ConfigurationChangeEvent): void {
if (!e || e.affectsConfiguration('signature')) {
let config = workspace.getConfiguration('signature')
let target = config.get<string>('target', 'float')
if (target == 'float' && !workspace.floatSupported) {
target = 'echo'
}
this.config = {
target,
floatConfig: config.get('floatConfig', {}),
trigger: config.get<boolean>('enable', true),
wait: Math.max(config.get<number>('triggerSignatureWait', 500), 200),
preferAbove: config.get<boolean>('preferShownAbove', true),
hideOnChange: config.get<boolean>('hideOnTextChange', false),
}
}
}
public async triggerSignatureHelp(): Promise<boolean> {
let { doc, position } = await this.handler.getCurrentState()
if (!languages.hasProvider('signature', doc.textDocument)) return false
return await this._triggerSignatureHelp(doc, position, true, 0)
}
private async _triggerSignatureHelp(doc: Document, position: Position, invoke = true, offset = 0): Promise<boolean> {
this.tokenSource?.cancel()
let tokenSource = this.tokenSource = new CancellationTokenSource()
let token = tokenSource.token
token.onCancellationRequested(() => {
tokenSource.dispose()
this.tokenSource = undefined
})
let { target } = this.config
let timer = this.timer = setTimeout(() => {
tokenSource.cancel()
}, this.config.wait)
await doc.patchChange(true)
let signatureHelp = await languages.getSignatureHelp(doc.textDocument, position, token, {
isRetrigger: this.signatureFactory.checkRetrigger(doc.bufnr),
triggerKind: invoke ? SignatureHelpTriggerKind.Invoked : SignatureHelpTriggerKind.TriggerCharacter
})
clearTimeout(timer)
if (token.isCancellationRequested) return false
if (!signatureHelp || signatureHelp.signatures.length == 0) {
this.signatureFactory.close()
return false
}
let { activeSignature, signatures } = signatureHelp
if (activeSignature) {
// make active first
let [active] = signatures.splice(activeSignature, 1)
if (active) signatures.unshift(active)
}
if (target == 'echo') {
this.echoSignature(signatureHelp)
} else {
await this.showSignatureHelp(doc, position, signatureHelp, offset)
}
return true
}
private async showSignatureHelp(doc: Document, position: Position, signatureHelp: SignatureHelp, offset: number): Promise<void> {
let { signatures, activeParameter } = signatureHelp
let paramDoc: string | MarkupContent = null
let startOffset = offset
let docs = signatures.reduce((p, c, idx) => {
let activeIndexes: [number, number] = null
let activeIndex = c.activeParameter ?? typeof activeParameter === 'number' ? activeParameter : undefined
if (activeIndex === undefined && c.parameters?.length > 0) {
activeIndex = 0
}
let nameIndex = c.label.indexOf('(')
if (idx == 0 && typeof activeIndex === 'number') {
let active = c.parameters?.[activeIndex]
if (active) {
let after = c.label.slice(nameIndex == -1 ? 0 : nameIndex)
paramDoc = active.documentation
if (typeof active.label === 'string') {
let str = after.slice(0)
let ms = str.match(new RegExp('\\b' + active.label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b'))
let index = ms ? ms.index : str.indexOf(active.label)
if (index != -1) {
activeIndexes = [
index + nameIndex,
index + active.label.length + nameIndex
]
}
} else {
activeIndexes = active.label
}
}
}
if (activeIndexes == null) {
activeIndexes = [nameIndex + 1, nameIndex + 1]
}
if (offset == startOffset) {
offset = offset + activeIndexes[0] + 1
}
p.push({
content: c.label,
filetype: doc.filetype,
active: activeIndexes
})
if (paramDoc) {
let content = typeof paramDoc === 'string' ? paramDoc : paramDoc.value
if (content.trim().length) {
p.push({
content,
filetype: isMarkdown(c.documentation) ? 'markdown' : 'txt'
})
}
}
if (idx == 0 && c.documentation) {
let { documentation } = c
let content = typeof documentation === 'string' ? documentation : documentation.value
if (content.trim().length) {
p.push({
content,
filetype: isMarkdown(c.documentation) ? 'markdown' : 'txt'
})
}
}
return p
}, [])
let content = doc.getline(position.line, false).slice(0, position.character)
this.lastPosition = { bufnr: doc.bufnr, lnum: position.line + 1, col: byteLength(content) + 1 }
const excludeImages = workspace.getConfiguration('coc.preferences').get<boolean>('excludeImageLinksInMarkdownDocument')
let config = this.signatureFactory.applyFloatConfig({
preferTop: this.config.preferAbove,
autoHide: false,
offsetX: offset,
modes: ['i', 'ic', 's'],
excludeImages
}, this.config.floatConfig)
await this.signatureFactory.show(docs, config)
}
private echoSignature(signatureHelp: SignatureHelp): void {
let { signatures, activeParameter } = signatureHelp
let columns = workspace.env.columns
signatures = signatures.slice(0, workspace.env.cmdheight)
let signatureList: SignaturePart[][] = []
for (let signature of signatures) {
let parts: SignaturePart[] = []
let { label } = signature
label = label.replace(/\n/g, ' ')
if (label.length >= columns - 16) {
label = label.slice(0, columns - 16) + '...'
}
let nameIndex = label.indexOf('(')
if (nameIndex == -1) {
parts = [{ text: label, type: 'Normal' }]
} else {
parts.push({
text: label.slice(0, nameIndex),
type: 'Label'
})
let after = label.slice(nameIndex)
if (signatureList.length == 0 && activeParameter != null) {
let active = signature.parameters?.[activeParameter]
if (active) {
let start: number
let end: number
if (typeof active.label === 'string') {
let str = after.slice(0)
let ms = str.match(new RegExp('\\b' + active.label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b'))
let idx = ms ? ms.index : str.indexOf(active.label)
if (idx == -1) {
parts.push({ text: after, type: 'Normal' })
} else {
start = idx
end = idx + active.label.length
}
} else {
[start, end] = active.label
start = start - nameIndex
end = end - nameIndex
}
if (start != null && end != null) {
parts.push({ text: after.slice(0, start), type: 'Normal' })
parts.push({ text: after.slice(start, end), type: 'MoreMsg' })
parts.push({ text: after.slice(end), type: 'Normal' })
}
}
} else {
parts.push({
text: after,
type: 'Normal'
})
}
}
signatureList.push(parts)
}
this.nvim.callTimer('coc#ui#echo_signatures', [signatureList], true)
}
public dispose(): void {
disposeAll(this.disposables)
if (this.timer) {
clearTimeout(this.timer)
}
}
}