coc.nvim
Version:
LSP based intellisense engine for neovim & vim8.
610 lines • 22 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 vscode_uri_1 = require("vscode-uri");
const diff_1 = require("../util/diff");
const fs_1 = require("../util/fs");
const index_1 = require("../util/index");
const string_1 = require("../util/string");
const chars_1 = require("./chars");
const array_1 = require("../util/array");
const position_1 = require("../util/position");
const logger = require('../util/logger')('model-document');
// wrapper class of TextDocument
class Document {
constructor(buffer, env) {
this.buffer = buffer;
this.env = env;
this.paused = false;
this.isIgnored = false;
// start id for matchaddpos
this.colorId = 1080;
this.eol = true;
this.attached = false;
// real current lines
this.lines = [];
this._additionalKeywords = [];
this._words = [];
this._onDocumentChange = new vscode_languageserver_protocol_1.Emitter();
this._onDocumentDetach = new vscode_languageserver_protocol_1.Emitter();
this.onDocumentChange = this._onDocumentChange.event;
this.onDocumentDetach = this._onDocumentDetach.event;
this.fireContentChanges = debounce_1.default(() => {
this._fireContentChanges();
}, 200);
this.fetchContent = debounce_1.default(() => {
this._fetchContent().catch(e => {
logger.error(`Error on fetch content:`, e);
});
}, 50);
}
get shouldAttach() {
let { buftype } = this;
if (this.uri.endsWith('%5BCommand%20Line%5D'))
return true;
return buftype == '' || buftype == 'acwrite';
}
get words() {
return this._words;
}
setFiletype(filetype) {
let { uri, version } = this;
this._filetype = this.convertFiletype(filetype);
version = version ? version + 1 : 1;
let textDocument = vscode_languageserver_protocol_1.TextDocument.create(uri, this.filetype, version, this.content);
this.textDocument = textDocument;
}
convertFiletype(filetype) {
let map = this.env.filetypeMap;
if (filetype == 'json' && this.uri && this.uri.endsWith('coc-settings.json')) {
return 'jsonc';
}
if (filetype == 'javascript.jsx')
return 'javascriptreact';
if (filetype == 'typescript.jsx' || filetype == 'typescript.tsx')
return 'typescriptreact';
return map[filetype] || filetype;
}
/**
* Current changedtick of buffer
*
* @public
* @returns {number}
*/
get changedtick() {
return this._changedtick;
}
get schema() {
return vscode_uri_1.URI.parse(this.uri).scheme;
}
get lineCount() {
return this.lines.length;
}
async init(nvim, token) {
this.nvim = nvim;
let { buffer } = this;
let opts = await nvim.call('coc#util#get_bufoptions', buffer.id);
if (opts == null)
return false;
let buftype = this.buftype = opts.buftype;
this.variables = opts.variables;
this._additionalKeywords = opts.additionalKeywords;
this._changedtick = opts.changedtick;
this._rootPatterns = opts.rootPatterns;
this.eol = opts.eol == 1;
let uri = this._uri = index_1.getUri(opts.fullpath, buffer.id, buftype);
token.onCancellationRequested(() => {
this.detach();
});
try {
if (!this.env.isVim) {
let res = await this.attach();
if (!res)
return false;
}
else {
this.lines = await buffer.lines;
}
this.attached = true;
}
catch (e) {
logger.error('attach error:', e);
return false;
}
this._filetype = this.convertFiletype(opts.filetype);
this.textDocument = vscode_languageserver_protocol_1.TextDocument.create(uri, this.filetype, 1, this.getDocumentContent());
this.setIskeyword(opts.iskeyword);
this.gitCheck();
if (token.isCancellationRequested)
return false;
return true;
}
setIskeyword(iskeyword) {
let chars = (this.chars = new chars_1.Chars(iskeyword));
for (let ch of this._additionalKeywords) {
chars.addKeyword(ch);
}
this._words = this.chars.matchKeywords(this.lines.join('\n'));
}
async attach() {
if (this.shouldAttach) {
let attached = await this.buffer.attach(false);
if (!attached)
return false;
this.lines = await this.buffer.lines;
}
else {
this.lines = await this.buffer.lines;
return true;
}
if (!this.buffer.isAttached)
return;
this.buffer.listen('lines', (...args) => {
this.onChange.apply(this, args);
});
this.buffer.listen('detach', async () => {
await index_1.wait(30);
if (!this.attached)
return;
// it could be detached by `edit!`
let attached = await this.attach();
if (!attached)
this.detach();
});
this.buffer.listen('changedtick', (_buf, tick) => {
this._changedtick = tick;
});
if (this.textDocument) {
this.fireContentChanges();
}
return true;
}
onChange(buf, tick, firstline, lastline, linedata
// more:boolean
) {
if (buf.id !== this.buffer.id || tick == null)
return;
this._changedtick = tick;
let lines = this.lines.slice(0, firstline);
lines = lines.concat(linedata, this.lines.slice(lastline));
this.lines = lines;
this.fireContentChanges();
}
/**
* Make sure current document synced correctly
*
* @public
* @returns {Promise<void>}
*/
async checkDocument() {
this.paused = false;
let { buffer } = this;
this._changedtick = await buffer.changedtick;
this.lines = await buffer.lines;
this.fireContentChanges.clear();
this._fireContentChanges();
}
get dirty() {
return this.content != this.getDocumentContent();
}
_fireContentChanges(force = false) {
let { paused, textDocument } = this;
if (paused && !force)
return;
try {
let content = this.getDocumentContent();
let change = diff_1.getChange(this.content, content);
if (change == null)
return;
this.createDocument();
let { version, uri } = this;
let start = textDocument.positionAt(change.start);
let end = textDocument.positionAt(change.end);
let changes = [{
range: { start, end },
rangeLength: change.end - change.start,
text: change.newText
}];
logger.debug('changes:', JSON.stringify(changes, null, 2));
this._onDocumentChange.fire({
textDocument: { version, uri },
contentChanges: changes
});
this._words = this.chars.matchKeywords(this.lines.join('\n'));
}
catch (e) {
logger.error(e.message);
}
}
detach() {
// neovim not detach on `:checktime`
if (this.attached) {
this.attached = false;
this.buffer.detach().catch(_e => {
// noop
});
this._onDocumentDetach.fire(this.uri);
}
this.fetchContent.clear();
this.fireContentChanges.clear();
this._onDocumentChange.dispose();
this._onDocumentDetach.dispose();
}
get bufnr() {
return this.buffer.id;
}
get content() {
return this.textDocument.getText();
}
get filetype() {
return this._filetype;
}
get uri() {
return this._uri;
}
get version() {
return this.textDocument ? this.textDocument.version : null;
}
async applyEdits(nvim, edits, sync = true) {
if (Array.isArray(nvim)) {
sync = edits == null ? true : edits;
edits = nvim;
}
if (edits.length == 0)
return;
let orig = this.lines.join('\n') + (this.eol ? '\n' : '');
let textDocument = vscode_languageserver_protocol_1.TextDocument.create(this.uri, this.filetype, 1, orig);
let content = vscode_languageserver_protocol_1.TextDocument.applyEdits(textDocument, edits);
// could be equal sometimes
if (orig === content) {
this.createDocument();
}
else {
let d = diff_1.diffLines(orig, content);
await this.buffer.setLines(d.replacement, {
start: d.start,
end: d.end,
strictIndexing: false
});
// can't wait vim sync buffer
this.lines = (this.eol && content.endsWith('\n') ? content.slice(0, -1) : content).split('\n');
if (sync)
this.forceSync();
}
}
forceSync(ignorePause = true) {
this.fireContentChanges.clear();
this._fireContentChanges(ignorePause);
}
getOffset(lnum, col) {
return this.textDocument.offsetAt({
line: lnum - 1,
character: col
});
}
isWord(word) {
return this.chars.isKeyword(word);
}
getMoreWords() {
let res = [];
let { words, chars } = this;
if (!chars.isKeywordChar('-'))
return res;
for (let word of words) {
word = word.replace(/^-+/, '');
if (word.indexOf('-') !== -1) {
let parts = word.split('-');
for (let part of parts) {
if (part.length > 2 &&
res.indexOf(part) === -1 &&
words.indexOf(part) === -1) {
res.push(part);
}
}
}
}
return res;
}
/**
* Current word for replacement
*/
getWordRangeAtPosition(position, extraChars, current = true) {
let chars = this.chars.clone();
if (extraChars && extraChars.length) {
for (let ch of extraChars) {
chars.addKeyword(ch);
}
}
let line = this.getline(position.line, current);
if (line.length == 0 || position.character >= line.length)
return null;
if (!chars.isKeywordChar(line[position.character]))
return null;
let start = position.character;
let end = position.character + 1;
if (!chars.isKeywordChar(line[start])) {
return vscode_languageserver_protocol_1.Range.create(position, { line: position.line, character: position.character + 1 });
}
while (start >= 0) {
let ch = line[start - 1];
if (!ch || !chars.isKeyword(ch))
break;
start = start - 1;
}
while (end <= line.length) {
let ch = line[end];
if (!ch || !chars.isKeywordChar(ch))
break;
end = end + 1;
}
return vscode_languageserver_protocol_1.Range.create(position.line, start, position.line, end);
}
gitCheck() {
let { uri } = this;
if (!uri.startsWith('file') || this.buftype != '')
return;
let filepath = vscode_uri_1.URI.parse(uri).fsPath;
fs_1.isGitIgnored(filepath).then(isIgnored => {
this.isIgnored = isIgnored;
}, () => {
this.isIgnored = false;
});
}
createDocument(changeCount = 1) {
let { version, uri, filetype } = this;
version = version + changeCount;
this.textDocument = vscode_languageserver_protocol_1.TextDocument.create(uri, filetype, version, this.getDocumentContent());
}
async _fetchContent() {
if (!this.env.isVim || !this.attached)
return;
let { nvim, buffer } = this;
let { id } = buffer;
let o = (await nvim.call('coc#util#get_content', id));
if (!o)
return;
let { content, changedtick } = o;
this._changedtick = changedtick;
let newLines = content.split('\n');
this.lines = newLines;
this._fireContentChanges();
}
async patchChange() {
if (!this.env.isVim || !this.attached)
return;
let change = await this.nvim.call('coc#util#get_changeinfo', []);
if (change.changedtick == this._changedtick)
return;
let { lines } = this;
let { lnum, line, changedtick } = change;
this._changedtick = changedtick;
lines[lnum - 1] = line;
}
getSymbolRanges(word) {
this.forceSync();
let { textDocument } = this;
let res = [];
let content = textDocument.getText();
let str = '';
for (let i = 0, l = content.length; i < l; i++) {
let ch = content[i];
if ('-' == ch && str.length == 0) {
continue;
}
let isKeyword = this.chars.isKeywordChar(ch);
if (isKeyword) {
str = str + ch;
}
if (str.length > 0 && !isKeyword && str == word) {
res.push(vscode_languageserver_protocol_1.Range.create(textDocument.positionAt(i - str.length), textDocument.positionAt(i)));
}
if (!isKeyword) {
str = '';
}
}
return res;
}
async patchChangedTick() {
if (!this.env.isVim || !this.attached)
return;
this._changedtick = await this.nvim.call('getbufvar', [this.bufnr, 'changedtick']);
}
fixStartcol(position, valids) {
let line = this.getline(position.line);
if (!line)
return null;
let { character } = position;
let start = line.slice(0, character);
let col = string_1.byteLength(start);
let { chars } = this;
for (let i = start.length - 1; i >= 0; i--) {
let c = start[i];
if (c == ' ')
break;
if (!chars.isKeywordChar(c) && valids.indexOf(c) === -1) {
break;
}
col = col - string_1.byteLength(c);
}
return col;
}
/**
* Use matchaddpos for highlight ranges, must use `redraw` command on vim
*/
matchAddRanges(ranges, hlGroup, priority = 10) {
let res = [];
let arr = [];
let splited = ranges.reduce((p, c) => {
for (let i = c.start.line; i <= c.end.line; i++) {
let curr = this.getline(i) || '';
let sc = i == c.start.line ? c.start.character : 0;
let ec = i == c.end.line ? c.end.character : curr.length;
if (sc == ec)
continue;
p.push(vscode_languageserver_protocol_1.Range.create(i, sc, i, ec));
}
return p;
}, []);
for (let range of splited) {
let { start, end } = range;
if (start.character == end.character)
continue;
let line = this.getline(start.line);
arr.push([start.line + 1, string_1.byteIndex(line, start.character) + 1, string_1.byteLength(line.slice(start.character, end.character))]);
}
for (let grouped of array_1.group(arr, 8)) {
let id = this.colorId;
this.colorId = this.colorId + 1;
this.nvim.call('matchaddpos', [hlGroup, grouped, priority, id], true);
res.push(id);
}
this.nvim.call('coc#util#add_matchids', [res], true);
return res;
}
highlightRanges(ranges, hlGroup, srcId) {
let res = [];
if (this.env.isVim && !this.env.textprop) {
res = this.matchAddRanges(ranges, hlGroup, 10);
}
else {
let lineRanges = [];
for (let range of ranges) {
if (range.start.line == range.end.line) {
lineRanges.push(range);
}
else {
// split range by lines
for (let i = range.start.line; i < range.end.line; i++) {
let line = this.getline(i);
if (i == range.start.line) {
lineRanges.push(vscode_languageserver_protocol_1.Range.create(i, range.start.character, i, line.length));
}
else if (i == range.end.line) {
lineRanges.push(vscode_languageserver_protocol_1.Range.create(i, Math.min(line.match(/^\s*/)[0].length, range.end.character), i, range.end.character));
}
else {
lineRanges.push(vscode_languageserver_protocol_1.Range.create(i, Math.min(line.match(/^\s*/)[0].length, line.length), i, line.length));
}
}
}
}
for (let range of lineRanges) {
let { start, end } = range;
if (position_1.comparePosition(start, end) == 0)
continue;
let line = this.getline(start.line);
// tslint:disable-next-line: no-floating-promises
this.buffer.addHighlight({
hlGroup,
srcId,
line: start.line,
colStart: string_1.byteIndex(line, start.character),
colEnd: end.line - start.line == 1 && end.character == 0 ? -1 : string_1.byteIndex(line, end.character)
});
}
res.push(srcId);
}
return res;
}
clearMatchIds(ids) {
if (this.env.isVim && !this.env.textprop) {
this.nvim.call('coc#util#clearmatches', [Array.from(ids)], true);
}
else {
for (let id of ids) {
if (this.nvim.hasFunction('nvim_create_namespace')) {
this.buffer.clearNamespace(id);
}
else {
this.buffer.clearHighlight({ srcId: id });
}
}
}
}
async getcwd() {
let wid = await this.nvim.call('bufwinid', this.buffer.id);
if (wid == -1)
return await this.nvim.call('getcwd');
return await this.nvim.call('getcwd', wid);
}
getLocalifyBonus(sp, ep) {
let res = new Map();
let { chars } = this;
let startLine = Math.max(0, sp.line - 100);
let endLine = Math.min(this.lineCount, sp.line + 100);
let content = this.lines.slice(startLine, endLine).join('\n');
sp = vscode_languageserver_protocol_1.Position.create(sp.line - startLine, sp.character);
ep = vscode_languageserver_protocol_1.Position.create(ep.line - startLine, ep.character);
let doc = vscode_languageserver_protocol_1.TextDocument.create(this.uri, this.filetype, 1, content);
let headCount = doc.offsetAt(sp);
let len = content.length;
let tailCount = len - doc.offsetAt(ep);
let start = 0;
let preKeyword = false;
for (let i = 0; i < headCount; i++) {
let iskeyword = chars.isKeyword(content[i]);
if (!preKeyword && iskeyword) {
start = i;
}
else if (preKeyword && (!iskeyword || i == headCount - 1)) {
if (i - start > 1) {
let str = content.slice(start, i);
res.set(str, i / headCount);
}
}
preKeyword = iskeyword;
}
start = len - tailCount;
preKeyword = false;
for (let i = start; i < content.length; i++) {
let iskeyword = chars.isKeyword(content[i]);
if (!preKeyword && iskeyword) {
start = i;
}
else if (preKeyword && (!iskeyword || i == len - 1)) {
if (i - start > 1) {
let end = i == len - 1 ? i + 1 : i;
let str = content.slice(start, end);
let score = res.get(str) || 0;
res.set(str, Math.max(score, (len - i + (end - start)) / tailCount));
}
}
preKeyword = iskeyword;
}
return res;
}
/**
* Real current line
*/
getline(line, current = true) {
if (current)
return this.lines[line] || '';
let lines = this.textDocument.getText().split(/\r?\n/);
return lines[line] || '';
}
getLines(start, end) {
return this.lines.slice(start, end);
}
getDocumentContent() {
let content = this.lines.join('\n');
return this.eol ? content + '\n' : content;
}
getVar(key, defaultValue) {
let val = this.variables[`coc_${key}`];
return val === undefined ? defaultValue : val;
}
get rootPatterns() {
return this._rootPatterns;
}
getPosition(lnum, col) {
let line = this.getline(lnum - 1);
if (!line || col == 0)
return { line: lnum - 1, character: 0 };
let pre = string_1.byteSlice(line, 0, col - 1);
return { line: lnum - 1, character: pre.length };
}
}
exports.default = Document;
//# sourceMappingURL=document.js.map