coc.nvim
Version:
LSP based intellisense engine for neovim & vim8.
597 lines • 23.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const debounce_1 = tslib_1.__importDefault(require("debounce"));
const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
const events_1 = tslib_1.__importDefault(require("../events"));
const sources_1 = tslib_1.__importDefault(require("../sources"));
const util_1 = require("../util");
const string_1 = require("../util/string");
const workspace_1 = tslib_1.__importDefault(require("../workspace"));
const complete_1 = tslib_1.__importDefault(require("./complete"));
const floating_1 = tslib_1.__importDefault(require("./floating"));
const logger = require('../util/logger')('completion');
const completeItemKeys = ['abbr', 'menu', 'info', 'kind', 'icase', 'dup', 'empty', 'user_data'];
class Completion {
constructor() {
// current input string
this.activted = false;
this.disposables = [];
this.complete = null;
this.recentScores = {};
this.changedTick = 0;
this.insertCharTs = 0;
this.insertLeaveTs = 0;
// only used when no pum change event
this.isResolving = false;
}
init(nvim) {
this.nvim = nvim;
this.config = this.getCompleteConfig();
this.floating = new floating_1.default(nvim);
events_1.default.on('InsertCharPre', this.onInsertCharPre, this, this.disposables);
events_1.default.on('InsertLeave', this.onInsertLeave, this, this.disposables);
events_1.default.on('InsertEnter', this.onInsertEnter, this, this.disposables);
events_1.default.on('TextChangedP', this.onTextChangedP, this, this.disposables);
events_1.default.on('TextChangedI', this.onTextChangedI, this, this.disposables);
events_1.default.on('CompleteDone', this.onCompleteDone, this, this.disposables);
events_1.default.on('MenuPopupChanged', this.onPumChange, this, this.disposables);
events_1.default.on('CursorMovedI', debounce_1.default(async (bufnr, cursor) => {
// try trigger completion
let doc = workspace_1.default.getDocument(bufnr);
if (this.isActivted || !doc || cursor[1] == 1)
return;
let line = doc.getline(cursor[0] - 1);
if (!line)
return;
let { latestInsertChar } = this;
let pre = string_1.byteSlice(line, 0, cursor[1] - 1);
if (!latestInsertChar || !pre.endsWith(latestInsertChar))
return;
if (sources_1.default.shouldTrigger(pre, doc.filetype)) {
await this.triggerCompletion(doc, pre, false);
}
}, 20));
workspace_1.default.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('suggest')) {
Object.assign(this.config, this.getCompleteConfig());
}
}, null, this.disposables);
}
get option() {
if (!this.complete)
return null;
return this.complete.option;
}
addRecent(word, bufnr) {
if (!word)
return;
this.recentScores[`${bufnr}|${word}`] = Date.now();
}
async getPreviousContent(document) {
let [, lnum, col] = await this.nvim.call('getcurpos');
if (this.option && lnum != this.option.linenr)
return null;
let line = document.getline(lnum - 1);
return col == 1 ? '' : string_1.byteSlice(line, 0, col - 1);
}
getResumeInput(pre) {
let { option, activted } = this;
if (!activted)
return null;
if (!pre)
return '';
let input = string_1.byteSlice(pre, option.col);
if (option.blacklist && option.blacklist.indexOf(input) !== -1)
return null;
return input;
}
get bufnr() {
let { option } = this;
return option ? option.bufnr : null;
}
get isActivted() {
return this.activted;
}
getCompleteConfig() {
let config = workspace_1.default.getConfiguration('coc.preferences');
let suggest = workspace_1.default.getConfiguration('suggest');
function getConfig(key, defaultValue) {
return config.get(key, suggest.get(key, defaultValue));
}
let keepCompleteopt = getConfig('keepCompleteopt', false);
let autoTrigger = getConfig('autoTrigger', 'always');
if (keepCompleteopt) {
let { completeOpt } = workspace_1.default;
if (!completeOpt.includes('noinsert') && !completeOpt.includes('noselect')) {
autoTrigger = 'none';
}
}
let acceptSuggestionOnCommitCharacter = workspace_1.default.env.pumevent && getConfig('acceptSuggestionOnCommitCharacter', false);
return {
autoTrigger,
keepCompleteopt,
disableMenuShortcut: getConfig('disableMenuShortcut', false),
acceptSuggestionOnCommitCharacter,
disableKind: getConfig('disableKind', false),
disableMenu: getConfig('disableMenu', false),
previewIsKeyword: getConfig('previewIsKeyword', '@,48-57,_192-255'),
enablePreview: getConfig('enablePreview', false),
enablePreselect: getConfig('enablePreselect', false),
maxPreviewWidth: getConfig('maxPreviewWidth', 50),
labelMaxLength: getConfig('labelMaxLength', 100),
triggerAfterInsertEnter: getConfig('triggerAfterInsertEnter', false),
noselect: getConfig('noselect', true),
numberSelect: getConfig('numberSelect', false),
maxItemCount: getConfig('maxCompleteItemCount', 50),
timeout: getConfig('timeout', 500),
minTriggerInputLength: getConfig('minTriggerInputLength', 1),
snippetIndicator: getConfig('snippetIndicator', '~'),
fixInsertedWord: getConfig('fixInsertedWord', true),
localityBonus: getConfig('localityBonus', true),
highPrioritySourceLimit: getConfig('highPrioritySourceLimit', null),
lowPrioritySourceLimit: getConfig('lowPrioritySourceLimit', null),
};
}
async startCompletion(option) {
workspace_1.default.bufnr = option.bufnr;
let document = workspace_1.default.getDocument(option.bufnr);
if (!document)
return;
// use fixed filetype
option.filetype = document.filetype;
this.document = document;
try {
await this._doComplete(option);
}
catch (e) {
this.stop();
workspace_1.default.showMessage(`Error happens on complete: ${e.message}`, 'error');
logger.error(e.stack);
}
}
async resumeCompletion(pre, search, force = false) {
let { document, complete, activted } = this;
if (!activted || !complete.results)
return;
if (search == this.input && !force)
return;
let last = search == null ? '' : search.slice(-1);
if (last.length == 0 ||
/\s/.test(last) ||
sources_1.default.shouldTrigger(pre, document.filetype) ||
search.length < complete.input.length) {
this.stop();
return;
}
let { changedtick } = document;
this.input = search;
let items;
if (complete.isIncomplete && document.chars.isKeywordChar(last)) {
await document.patchChange();
document.forceSync();
await util_1.wait(30);
if (document.changedtick != changedtick)
return;
items = await complete.completeInComplete(search);
if (document.changedtick != changedtick)
return;
}
else {
items = complete.filterResults(search);
}
if (!this.isActivted)
return;
if (!complete.isCompleting && (!items || items.length === 0)) {
this.stop();
return;
}
await this.showCompletion(this.option.col, items);
}
hasSelected() {
if (workspace_1.default.env.pumevent)
return this.currItem != null;
if (this.config.noselect === false)
return true;
return this.isResolving;
}
async showCompletion(col, items) {
let { nvim, document, option } = this;
let { numberSelect, disableKind, labelMaxLength, disableMenuShortcut, disableMenu } = this.config;
let preselect = this.config.enablePreselect ? items.findIndex(o => o.preselect == true) : -1;
if (numberSelect && !/^\d/.test(option.input)) {
items = items.map((item, i) => {
let idx = i + 1;
if (i < 9) {
return Object.assign({}, item, {
abbr: item.abbr ? `${idx} ${item.abbr}` : `${idx} ${item.word}`
});
}
return item;
});
nvim.call('coc#_map', [], true);
}
this.changedTick = document.changedtick;
let validKeys = completeItemKeys.slice();
if (disableKind)
validKeys = validKeys.filter(s => s != 'kind');
if (disableMenu)
validKeys = validKeys.filter(s => s != 'menu');
let vimItems = items.map(item => {
let obj = { word: item.word, equal: 1 };
for (let key of validKeys) {
if (item.hasOwnProperty(key)) {
if (disableMenuShortcut && key == 'menu') {
obj[key] = item[key].replace(/\[\w+\]$/, '');
}
else if (key == 'abbr' && item[key].length > labelMaxLength) {
obj[key] = item[key].slice(0, labelMaxLength);
}
else {
obj[key] = item[key];
}
}
}
return obj;
});
nvim.call('coc#_do_complete', [col, vimItems, preselect], true);
}
async _doComplete(option) {
let { source } = option;
let { nvim, config, document } = this;
// current input
this.input = option.input;
let arr = [];
if (source == null) {
arr = sources_1.default.getCompleteSources(option);
}
else {
let s = sources_1.default.getSource(source);
if (s)
arr.push(s);
}
if (!arr.length)
return;
let complete = new complete_1.default(option, document, this.recentScores, config, arr, nvim);
this.start(complete);
let items = await this.complete.doComplete();
if (complete.isCanceled)
return;
if (items.length == 0 && !complete.isCompleting) {
this.stop();
return;
}
complete.onDidComplete(async () => {
let content = await this.getPreviousContent(document);
let search = this.getResumeInput(content);
if (complete.isCanceled)
return;
let hasSelected = this.hasSelected();
if (hasSelected && this.completeOpt.indexOf('noselect') !== -1)
return;
if (search == this.option.input) {
let items = complete.filterResults(search, Math.floor(Date.now() / 1000));
await this.showCompletion(option.col, items);
return;
}
await this.resumeCompletion(content, search, true);
});
if (items.length) {
let content = await this.getPreviousContent(document);
let search = this.getResumeInput(content);
if (complete.isCanceled)
return;
if (search == this.option.input) {
await this.showCompletion(option.col, items);
return;
}
await this.resumeCompletion(content, search, true);
}
}
async onTextChangedP() {
let { option, document } = this;
if (!option)
return;
await document.patchChange();
let hasInsert = this.latestInsert != null;
this.lastInsert = null;
// avoid trigger filter on pumvisible
if (document.changedtick == this.changedTick)
return;
let line = document.getline(option.linenr - 1);
let curr = line.match(/^\s*/)[0];
let ind = option.line.match(/^\s*/)[0];
// indent change
if (ind.length != curr.length) {
this.stop();
return;
}
if (!hasInsert) {
// this could be wrong, but can't avoid.
this.isResolving = true;
return;
}
let col = await this.nvim.call('col', '.');
let search = string_1.byteSlice(line, option.col, col - 1);
let pre = string_1.byteSlice(line, 0, col - 1);
if (sources_1.default.shouldTrigger(pre, document.filetype)) {
await this.triggerCompletion(document, pre, false);
}
else {
await this.resumeCompletion(pre, search);
}
}
async onTextChangedI(bufnr) {
let { nvim, latestInsertChar } = this;
this.lastInsert = null;
let document = workspace_1.default.getDocument(workspace_1.default.bufnr);
if (!document)
return;
await document.patchChange();
if (!this.isActivted) {
if (!latestInsertChar)
return;
let pre = await this.getPreviousContent(document);
await this.triggerCompletion(document, pre);
return;
}
if (bufnr !== this.bufnr)
return;
// check commit character
if (this.config.acceptSuggestionOnCommitCharacter
&& this.currItem
&& latestInsertChar
&& !this.document.isWord(latestInsertChar)) {
let resolvedItem = this.getCompleteItem(this.currItem);
if (sources_1.default.shouldCommit(resolvedItem, latestInsertChar)) {
let { linenr, col, line, colnr } = this.option;
this.stop();
let { word } = resolvedItem;
let newLine = `${line.slice(0, col)}${word}${latestInsertChar}${line.slice(colnr - 1)}`;
await nvim.call('coc#util#setline', [linenr, newLine]);
let curcol = col + word.length + 2;
await nvim.call('cursor', [linenr, curcol]);
return;
}
}
let content = await this.getPreviousContent(document);
if (content == null) {
// cursor line changed
this.stop();
return;
}
// check trigger character
if (sources_1.default.shouldTrigger(content, document.filetype)) {
await this.triggerCompletion(document, content, false);
return;
}
if (!this.isActivted || this.complete.isEmpty)
return;
let search = content.slice(string_1.characterIndex(content, this.option.col));
return await this.resumeCompletion(content, search);
}
async triggerCompletion(document, pre, checkTrigger = true) {
// check trigger
if (checkTrigger) {
let shouldTrigger = await this.shouldTrigger(document, pre);
if (!shouldTrigger)
return;
}
let option = await this.nvim.call('coc#util#get_complete_option');
if (!option)
return;
this.fixCompleteOption(option);
option.triggerCharacter = pre.slice(-1);
logger.debug('trigger completion with', option);
await this.startCompletion(option);
}
fixCompleteOption(opt) {
if (workspace_1.default.isVim) {
for (let key of ['word', 'input', 'line', 'filetype']) {
if (opt[key] == null) {
opt[key] = '';
}
}
}
}
async onCompleteDone(item) {
let { document } = this;
if (!this.isActivted || !document || !item.hasOwnProperty('word'))
return;
let opt = Object.assign({}, this.option);
let resolvedItem = this.getCompleteItem(item);
this.stop();
if (!resolvedItem)
return;
let timestamp = this.insertCharTs;
let insertLeaveTs = this.insertLeaveTs;
try {
await sources_1.default.doCompleteResolve(resolvedItem, (new vscode_languageserver_protocol_1.CancellationTokenSource()).token);
this.addRecent(resolvedItem.word, document.bufnr);
await util_1.wait(50);
if (this.insertCharTs != timestamp
|| this.insertLeaveTs != insertLeaveTs)
return;
await document.patchChange();
let content = await this.getPreviousContent(document);
if (!content.endsWith(resolvedItem.word))
return;
await sources_1.default.doCompleteDone(resolvedItem, opt);
document.forceSync();
}
catch (e) {
// tslint:disable-next-line:no-console
console.error(e.stack);
logger.error(`error on complete done`, e.stack);
}
}
async onInsertLeave(bufnr) {
this.insertLeaveTs = Date.now();
let doc = workspace_1.default.getDocument(bufnr);
if (doc)
doc.forceSync(true);
this.stop();
}
async onInsertEnter() {
if (!this.config.triggerAfterInsertEnter)
return;
let option = await this.nvim.call('coc#util#get_complete_option');
this.fixCompleteOption(option);
if (option && option.input.length >= this.config.minTriggerInputLength) {
await this.startCompletion(option);
}
}
async onInsertCharPre(character) {
this.lastInsert = {
character,
timestamp: Date.now(),
};
this.insertCharTs = this.lastInsert.timestamp;
}
get latestInsert() {
let { lastInsert } = this;
if (!lastInsert || Date.now() - lastInsert.timestamp > 200) {
return null;
}
return lastInsert;
}
get latestInsertChar() {
let { latestInsert } = this;
if (!latestInsert)
return '';
return latestInsert.character;
}
async shouldTrigger(document, pre) {
if (pre.length == 0 || /\s/.test(pre[pre.length - 1]))
return false;
let autoTrigger = this.config.autoTrigger;
if (autoTrigger == 'none')
return false;
if (sources_1.default.shouldTrigger(pre, document.filetype))
return true;
if (autoTrigger !== 'always')
return false;
let last = pre.slice(-1);
if (last && (document.isWord(pre.slice(-1)) || last.codePointAt(0) > 255)) {
let minLength = this.config.minTriggerInputLength;
if (minLength == 1)
return true;
let input = this.getInput(document, pre);
return input.length >= minLength;
}
return false;
}
async onPumChange(ev) {
if (!this.activted)
return;
if (this.document && this.document.uri.endsWith('%5BCommand%20Line%5D'))
return;
this.cancel();
let { completed_item, col, row, height, width, scrollbar } = ev;
let bounding = { col, row, height, width, scrollbar };
this.currItem = completed_item.hasOwnProperty('word') ? completed_item : null;
// it's pum change by vim, ignore it
if (this.lastInsert)
return;
let resolvedItem = this.getCompleteItem(completed_item);
if (!resolvedItem) {
this.floating.close();
return;
}
let source = this.resolveTokenSource = new vscode_languageserver_protocol_1.CancellationTokenSource();
let { token } = source;
await sources_1.default.doCompleteResolve(resolvedItem, token);
if (token.isCancellationRequested)
return;
let docs = resolvedItem.documentation;
if (!docs && resolvedItem.info) {
let { info } = resolvedItem;
let isText = /^[\w-\s.,\t]+$/.test(info);
docs = [{ filetype: isText ? 'txt' : this.document.filetype, content: info }];
}
if (!docs || docs.length == 0) {
this.floating.close();
}
else {
if (token.isCancellationRequested)
return;
await this.floating.show(docs, bounding, token);
}
this.resolveTokenSource = null;
}
start(complete) {
let { activted } = this;
this.activted = true;
this.isResolving = false;
if (activted) {
this.complete.dispose();
}
this.complete = complete;
if (!this.config.keepCompleteopt) {
this.nvim.command(`noa set completeopt=${this.completeOpt}`, true);
}
this.document.forceSync(true);
this.document.paused = true;
}
cancel() {
if (this.resolveTokenSource) {
this.resolveTokenSource.cancel();
this.resolveTokenSource = null;
}
}
stop() {
let { nvim } = this;
if (!this.activted)
return;
this.cancel();
this.currItem = null;
this.activted = false;
this.document.paused = false;
this.document.fireContentChanges();
if (this.complete) {
this.complete.dispose();
this.complete = null;
}
nvim.pauseNotification();
if (this.config.numberSelect) {
nvim.call('coc#_unmap', [], true);
}
if (!this.config.keepCompleteopt) {
this.nvim.command(`noa set completeopt=${workspace_1.default.completeOpt}`, true);
}
nvim.command(`let g:coc#_context['candidates'] = []`, true);
nvim.call('coc#_hide', [], true);
nvim.resumeNotification(false, true).catch(_e => {
// noop
});
}
getInput(document, pre) {
let input = '';
for (let i = pre.length - 1; i >= 0; i--) {
let ch = i == 0 ? null : pre[i - 1];
if (!ch || !document.isWord(ch)) {
input = pre.slice(i, pre.length);
break;
}
}
return input;
}
get completeOpt() {
let { noselect, enablePreview } = this.config;
let preview = enablePreview && !workspace_1.default.env.pumevent ? ',preview' : '';
if (noselect)
return `noselect,menuone${preview}`;
return `noinsert,menuone${preview}`;
}
getCompleteItem(item) {
if (!this.isActivted)
return null;
return this.complete.resolveCompletionItem(item);
}
dispose() {
util_1.disposeAll(this.disposables);
}
}
exports.Completion = Completion;
exports.default = new Completion();
//# sourceMappingURL=index.js.map