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

354 lines
14 KiB
TypeScript

'use strict'
import { CancellationToken, CompletionItem, CompletionItemKind, CompletionTriggerKind, DocumentSelector, InsertReplaceEdit, InsertTextFormat, Position, Range, TextEdit } from 'vscode-languageserver-protocol'
import commands from '../commands'
import Document from '../model/document'
import { CompletionItemProvider } from '../provider'
import snippetManager from '../snippets/manager'
import { SnippetParser } from '../snippets/parser'
import { CompleteOption, CompleteResult, ExtendedCompleteItem, ISource, SourceType } from '../types'
import { fuzzyMatch, getCharCodes } from '../util/fuzzy'
import { byteIndex, byteLength, byteSlice, characterIndex } from '../util/string'
import window from '../window'
import workspace from '../workspace'
const logger = require('../util/logger')('source-language')
export interface CompleteConfig {
labels: Map<CompletionItemKind, string>
snippetsSupport: boolean
defaultKindText: string
priority: number
detailMaxLength: number
detailField: string
invalidInsertCharacters: string[]
floatEnable: boolean
}
export default class LanguageSource implements ISource {
public priority: number
public sourceType: SourceType.Service
private _enabled = true
private filetype: string
private completeItems: CompletionItem[] = []
constructor(
public readonly name: string,
public readonly shortcut: string,
private provider: CompletionItemProvider,
public readonly documentSelector: DocumentSelector,
public readonly triggerCharacters: string[],
public readonly allCommitCharacters: string[],
priority: number | undefined,
private readonly completeConfig: CompleteConfig,
) {
this.priority = typeof priority === 'number' ? priority : completeConfig.priority
}
public get enable(): boolean {
return this._enabled
}
public toggle(): void {
this._enabled = !this._enabled
}
public shouldCommit?(item: ExtendedCompleteItem, character: string): boolean {
let completeItem = this.completeItems[item.index]
if (!completeItem) return false
let commitCharacters = [...this.allCommitCharacters, ...(completeItem.commitCharacters || [])]
return commitCharacters.includes(character)
}
public async doComplete(opt: CompleteOption, token: CancellationToken): Promise<CompleteResult | null> {
let { triggerCharacter, input, bufnr } = opt
this.filetype = opt.filetype
this.completeItems = []
let triggerKind: CompletionTriggerKind = this.getTriggerKind(opt)
let position = this.getPosition(opt)
let context: any = { triggerKind, option: opt }
if (triggerKind == CompletionTriggerKind.TriggerCharacter) context.triggerCharacter = triggerCharacter
let doc = workspace.getAttachedDocument(bufnr)
let result = await Promise.resolve(this.provider.provideCompletionItems(doc.textDocument, position, token, context))
if (!result || token.isCancellationRequested) return null
let completeItems = Array.isArray(result) ? result : result.items
if (!completeItems || completeItems.length == 0) return null
this.completeItems = completeItems
let startcol = getStartColumn(opt.line, completeItems)
let option: CompleteOption = Object.assign({}, opt)
let prefix: string
let isIncomplete = typeof result['isIncomplete'] === 'boolean' ? result['isIncomplete'] : false
if (startcol == null && input.length > 0 && this.triggerCharacters.includes(opt.triggerCharacter)) {
if (!completeItems.every(item => (item.insertText ?? item.label).startsWith(opt.input))) {
startcol = opt.col + byteLength(opt.input)
}
}
if (startcol != null) {
prefix = startcol < option.col ? byteSlice(opt.line, startcol, option.col) : ''
option.col = startcol
}
let items: ExtendedCompleteItem[] = completeItems.map((o, index) => {
let item = this.convertVimCompleteItem(o, this.shortcut, option, prefix)
item.index = index
return item
})
return { startcol, isIncomplete, items }
}
public async onCompleteResolve(item: ExtendedCompleteItem, token: CancellationToken): Promise<void> {
let { index } = item
let completeItem = this.completeItems[index]
if (!completeItem || item.resolved) return
let hasResolve = typeof this.provider.resolveCompletionItem === 'function'
if (hasResolve) {
let resolved = await Promise.resolve(this.provider.resolveCompletionItem(completeItem, token))
if (token.isCancellationRequested || !resolved) return
Object.assign(completeItem, resolved)
}
if (typeof item.documentation === 'undefined') {
let { documentation, detail } = completeItem
if (!documentation && !detail) return
let docs = []
if (detail && !item.detailShown && detail != item.word) {
detail = detail.replace(/\n\s*/g, ' ')
if (detail.length) {
let isText = /^[\w-\s.,\t\n]+$/.test(detail)
docs.push({ filetype: isText ? 'txt' : this.filetype, content: detail })
}
}
if (documentation) {
if (typeof documentation == 'string') {
docs.push({ filetype: 'txt', content: documentation })
} else if (documentation.value) {
docs.push({
filetype: documentation.kind == 'markdown' ? 'markdown' : 'txt',
content: documentation.value
})
}
}
item.resolved = true
item.documentation = docs
}
}
public async onCompleteDone(vimItem: ExtendedCompleteItem, opt: CompleteOption): Promise<void> {
let item = this.completeItems[vimItem.index]
if (!item) return
if (typeof vimItem.line === 'string') Object.assign(opt, { line: vimItem.line })
let doc = workspace.getAttachedDocument(opt.bufnr)
await doc.patchChange(true)
let additionalEdits = Array.isArray(item.additionalTextEdits) && item.additionalTextEdits.length > 0
if (additionalEdits) {
let shouldCancel = await snippetManager.editsInsideSnippet(item.additionalTextEdits)
if (shouldCancel) snippetManager.cancel()
}
let version = doc.version
let isSnippet = await this.applyTextEdit(doc, additionalEdits, item, vimItem.word, opt)
if (additionalEdits) {
// move cursor after edit
await doc.applyEdits(item.additionalTextEdits, doc.version != version, !isSnippet)
if (isSnippet) await snippetManager.selectCurrentPlaceholder()
}
if (item.command) {
if (commands.has(item.command.command)) {
await commands.execute(item.command)
} else {
logger.warn(`Command "${item.command.command}" not registered to coc.nvim`)
}
}
}
private async applyTextEdit(doc: Document, additionalEdits: boolean, item: CompletionItem, word: string, option: CompleteOption): Promise<boolean> {
let { line, linenr, colnr, col } = option
let pos = await window.getCursorPosition()
if (pos.line != linenr - 1) return
let { textEdit } = item
let currline = doc.getline(linenr - 1)
// before CompleteDone
let beginIdx = characterIndex(line, colnr - 1)
if (!textEdit && item.insertText) {
textEdit = {
range: Range.create(pos.line, characterIndex(line, col), pos.line, beginIdx),
newText: item.insertText
}
}
if (!textEdit) return false
let newText = textEdit.newText
let range = InsertReplaceEdit.is(textEdit) ? textEdit.replace : textEdit.range
// adjust range by indent
let n = fixIndent(line, currline, range)
if (n) beginIdx += n
// attempt to fix range from textEdit, range should include trigger position
if (range.end.character < beginIdx) range.end.character = beginIdx
// fix range by count cursor moved to replace insernt word on complete done.
if (pos.character > beginIdx) range.end.character += pos.character - beginIdx
let isSnippet = item.insertTextFormat === InsertTextFormat.Snippet
if (isSnippet && this.completeConfig.snippetsSupport === false) {
// could be wrong, but maybe best we can do.
isSnippet = false
newText = word
}
if (isSnippet) {
let opts = item.data?.ultisnip === true ? {} : item.data?.ultisnip
return await snippetManager.insertSnippet(newText, !additionalEdits, range, item.insertTextMode, opts ? opts : undefined)
}
await doc.applyEdits([TextEdit.replace(range, newText)], false, pos)
return false
}
private getTriggerKind(opt: CompleteOption): CompletionTriggerKind {
let { triggerCharacters } = this
let isTrigger = triggerCharacters.includes(opt.triggerCharacter)
let triggerKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
if (opt.triggerForInComplete) {
triggerKind = CompletionTriggerKind.TriggerForIncompleteCompletions
} else if (isTrigger) {
triggerKind = CompletionTriggerKind.TriggerCharacter
}
return triggerKind
}
private convertVimCompleteItem(item: CompletionItem, shortcut: string, opt: CompleteOption, prefix: string): ExtendedCompleteItem {
let { detailMaxLength, invalidInsertCharacters, detailField, labels, defaultKindText } = this.completeConfig
let hasAdditionalEdit = item.additionalTextEdits != null && item.additionalTextEdits.length > 0
let isSnippet = item.insertTextFormat === InsertTextFormat.Snippet || hasAdditionalEdit
let label = item.label.trim()
let obj: ExtendedCompleteItem = {
word: getWord(item, opt, invalidInsertCharacters),
abbr: label,
menu: `[${shortcut}]`,
kind: getKindString(item.kind, labels, defaultKindText),
sortText: item.sortText || null,
sourceScore: item['score'] || null,
filterText: item.filterText || label,
isSnippet,
dup: item.data && item.data.dup == 0 ? 0 : 1
}
if (prefix) {
if (!obj.filterText.startsWith(prefix)) {
if (item.textEdit && fuzzyMatch(getCharCodes(prefix), item.textEdit.newText)) {
obj.filterText = item.textEdit.newText.replace(/\r?\n/g, '')
}
}
if (!item.textEdit && !obj.word.startsWith(prefix)) {
// fix possible wrong word
obj.word = `${prefix}${obj.word}`
}
}
if (item && item.detail && detailField != 'preview') {
let detail = item.detail.replace(/\n\s*/g, ' ')
if (byteLength(detail) < detailMaxLength) {
if (detailField == 'menu') {
obj.menu = `${detail} ${obj.menu}`
} else if (detailField == 'abbr') {
obj.abbr = `${obj.abbr} - ${detail}`
}
obj.detailShown = 1
}
}
if (item.documentation) {
obj.info = typeof item.documentation == 'string' ? item.documentation : item.documentation.value
} else {
obj.info = ''
}
if (obj.word == '') obj.empty = 1
obj.line = opt.line
if (item.kind == CompletionItemKind.Folder && !obj.abbr.endsWith('/')) {
obj.abbr = obj.abbr + '/'
}
if (item.preselect) obj.preselect = true
if (item.data?.optional) obj.abbr = obj.abbr + '?'
return obj
}
private getPosition(opt: CompleteOption): Position {
let { line, linenr, colnr } = opt
let part = byteSlice(line, 0, colnr - 1)
return {
line: linenr - 1,
character: part.length
}
}
}
/*
* Check new startcol by check start characters.
*/
export function getStartColumn(line: string, items: CompletionItem[]): number | undefined {
let first = items[0]
if (first.textEdit == null) return undefined
let range = InsertReplaceEdit.is(first.textEdit) ? first.textEdit.replace : first.textEdit.range
let { character } = range.start
for (let i = 1; i < Math.min(10, items.length); i++) {
let o = items[i]
if (!o.textEdit) return undefined
let r = InsertReplaceEdit.is(o.textEdit) ? o.textEdit.replace : o.textEdit.range
if (r.start.character !== character) return undefined
}
return byteIndex(line, character)
}
export function getKindString(kind: CompletionItemKind, map: Map<CompletionItemKind, string>, defaultValue = ''): string {
return map.get(kind) || defaultValue
}
export function getWord(item: CompletionItem, opt: CompleteOption, invalidInsertCharacters: string[]): string {
let { label, data, insertTextFormat, insertText, textEdit } = item
let word: string
let newText: string
if (data && typeof data.word === 'string') return data.word
if (textEdit) {
let range = InsertReplaceEdit.is(textEdit) ? textEdit.replace : textEdit.range
newText = textEdit.newText
if (range && range.start.line == range.end.line) {
let { line, col, colnr } = opt
let character = characterIndex(line, col)
if (range.start.character > character) {
let before = line.slice(character, range.start.character)
newText = before + newText
} else {
let start = line.slice(range.start.character, character)
if (start.length && newText.startsWith(start)) {
newText = newText.slice(start.length)
}
}
character = characterIndex(line, colnr - 1)
if (range.end.character > character) {
let end = line.slice(character, range.end.character)
if (newText.endsWith(end)) {
newText = newText.slice(0, - end.length)
}
}
}
} else if (insertText) {
newText = insertText
}
if (insertTextFormat == InsertTextFormat.Snippet && newText && newText.includes('$')) {
let parser = new SnippetParser()
let text = parser.text(newText)
word = text ? getValidWord(text, invalidInsertCharacters) : label
} else {
word = getValidWord(newText, invalidInsertCharacters) || label
}
return word || ''
}
export function getValidWord(text: string, invalidChars: string[], start = 2): string {
if (!text) return ''
if (!invalidChars.length) return text
for (let i = start; i < text.length; i++) {
let c = text[i]
if (invalidChars.includes(c)) {
return text.slice(0, i)
}
}
return text
}
export function fixIndent(line: string, currline: string, range: Range): number {
let oldIndent = line.match(/^\s*/)[0]
let newIndent = currline.match(/^\s*/)[0]
if (oldIndent == newIndent) return
let d = newIndent.length - oldIndent.length
range.start.character += d
range.end.character += d
return d
}