UNPKG

coc.nvim

Version:

LSP based intellisense engine for neovim & vim8.

1,294 lines (1,293 loc) 61.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const debounce_1 = tslib_1.__importDefault(require("debounce")); const fs_1 = tslib_1.__importDefault(require("fs")); const os_1 = tslib_1.__importDefault(require("os")); const path_1 = tslib_1.__importDefault(require("path")); const util_1 = tslib_1.__importDefault(require("util")); const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol"); const vscode_uri_1 = require("vscode-uri"); const which_1 = tslib_1.__importDefault(require("which")); const configuration_1 = tslib_1.__importDefault(require("./configuration")); const shape_1 = tslib_1.__importDefault(require("./configuration/shape")); const events_1 = tslib_1.__importDefault(require("./events")); const db_1 = tslib_1.__importDefault(require("./model/db")); const document_1 = tslib_1.__importDefault(require("./model/document")); const fileSystemWatcher_1 = tslib_1.__importDefault(require("./model/fileSystemWatcher")); const mru_1 = tslib_1.__importDefault(require("./model/mru")); const outputChannel_1 = tslib_1.__importDefault(require("./model/outputChannel")); const resolver_1 = tslib_1.__importDefault(require("./model/resolver")); const status_1 = tslib_1.__importDefault(require("./model/status")); const task_1 = tslib_1.__importDefault(require("./model/task")); const terminal_1 = tslib_1.__importDefault(require("./model/terminal")); const willSaveHandler_1 = tslib_1.__importDefault(require("./model/willSaveHandler")); const types_1 = require("./types"); const array_1 = require("./util/array"); const fs_2 = require("./util/fs"); const index_1 = require("./util/index"); const match_1 = require("./util/match"); const position_1 = require("./util/position"); const string_1 = require("./util/string"); const watchman_1 = tslib_1.__importDefault(require("./watchman")); const uuid = require("uuid/v1"); const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; const logger = require('./util/logger')('workspace'); const CONFIG_FILE_NAME = 'coc-settings.json'; let NAME_SPACE = 1080; class Workspace { constructor() { this.keymaps = new Map(); this.resolver = new resolver_1.default(); this.rootPatterns = new Map(); this._workspaceFolders = []; this._insertMode = false; this._cwd = process.cwd(); this._blocking = false; this._initialized = false; this._attached = false; this.buffers = new Map(); this.autocmds = new Map(); this.terminals = new Map(); this.creatingSources = new Map(); this.outputChannels = new Map(); this.schemeProviderMap = new Map(); this.namespaceMap = new Map(); this.disposables = []; this.watchedOptions = new Set(); this._disposed = false; this._onDidOpenDocument = new vscode_languageserver_protocol_1.Emitter(); this._onDidCloseDocument = new vscode_languageserver_protocol_1.Emitter(); this._onDidChangeDocument = new vscode_languageserver_protocol_1.Emitter(); this._onWillSaveDocument = new vscode_languageserver_protocol_1.Emitter(); this._onDidSaveDocument = new vscode_languageserver_protocol_1.Emitter(); this._onDidChangeWorkspaceFolders = new vscode_languageserver_protocol_1.Emitter(); this._onDidChangeConfiguration = new vscode_languageserver_protocol_1.Emitter(); this._onDidWorkspaceInitialized = new vscode_languageserver_protocol_1.Emitter(); this._onDidOpenTerminal = new vscode_languageserver_protocol_1.Emitter(); this._onDidCloseTerminal = new vscode_languageserver_protocol_1.Emitter(); this.onDidCloseTerminal = this._onDidCloseTerminal.event; this.onDidOpenTerminal = this._onDidOpenTerminal.event; this.onDidChangeWorkspaceFolders = this._onDidChangeWorkspaceFolders.event; this.onDidOpenTextDocument = this._onDidOpenDocument.event; this.onDidCloseTextDocument = this._onDidCloseDocument.event; this.onDidChangeTextDocument = this._onDidChangeDocument.event; this.onWillSaveTextDocument = this._onWillSaveDocument.event; this.onDidSaveTextDocument = this._onDidSaveDocument.event; this.onDidChangeConfiguration = this._onDidChangeConfiguration.event; this.onDidWorkspaceInitialized = this._onDidWorkspaceInitialized.event; let json = requireFunc('../package.json'); this.version = json.version; this.configurations = this.createConfigurations(); this.willSaveUntilHandler = new willSaveHandler_1.default(this); this.setupDynamicAutocmd = debounce_1.default(() => { this._setupDynamicAutocmd().catch(e => { logger.error(e); }); }, global.hasOwnProperty('__TEST__') ? 0 : 100); this.setMessageLevel(); } async init() { let { nvim } = this; this.statusLine = new status_1.default(nvim); this._env = await nvim.call('coc#util#vim_info'); this._insertMode = this._env.mode.startsWith('insert'); if (this._env.workspaceFolders) { this._workspaceFolders = this._env.workspaceFolders.map(f => { return { uri: vscode_uri_1.URI.file(f).toString(), name: path_1.default.dirname(f) }; }); } this.checkProcess(); this.configurations.updateUserConfig(this._env.config); events_1.default.on('InsertEnter', () => { this._insertMode = true; }, null, this.disposables); events_1.default.on('InsertLeave', () => { this._insertMode = false; }, null, this.disposables); events_1.default.on('BufEnter', this.onBufEnter, this, this.disposables); events_1.default.on('CursorMoved', this.onCursorMoved, this, this.disposables); events_1.default.on('DirChanged', this.onDirChanged, this, this.disposables); events_1.default.on('BufCreate', this.onBufCreate, this, this.disposables); events_1.default.on('BufUnload', this.onBufUnload, this, this.disposables); events_1.default.on('TermOpen', this.onBufCreate, this, this.disposables); events_1.default.on('TermClose', this.onBufUnload, this, this.disposables); events_1.default.on('BufWritePost', this.onBufWritePost, this, this.disposables); events_1.default.on('BufWritePre', this.onBufWritePre, this, this.disposables); events_1.default.on('FileType', this.onFileTypeChange, this, this.disposables); events_1.default.on('CursorHold', this.checkBuffer, this, this.disposables); events_1.default.on('TextChanged', this.checkBuffer, this, this.disposables); events_1.default.on('BufReadCmd', this.onBufReadCmd, this, this.disposables); events_1.default.on('VimResized', (columns, lines) => { Object.assign(this._env, { columns, lines }); }, null, this.disposables); await this.attach(); this.initVimEvents(); this.configurations.onDidChange(e => { this._onDidChangeConfiguration.fire(e); }, null, this.disposables); this.watchOption('runtimepath', (_, newValue) => { this._env.runtimepath = newValue; }, this.disposables); this.watchOption('iskeyword', (_, newValue) => { let doc = this.getDocument(this.bufnr); if (doc) doc.setIskeyword(newValue); }, this.disposables); this.watchOption('completeopt', async (_, newValue) => { this.env.completeOpt = newValue; if (!this._attached) return; if (this.insertMode) { let suggest = this.getConfiguration('suggest'); if (suggest.get('autoTrigger') == 'always') { console.error(`Some plugin change completeopt on insert mode!`); // tslint:disable-line } } }, this.disposables); this.watchGlobal('coc_enabled', async (oldValue, newValue) => { if (newValue == oldValue) return; if (newValue == 1) { await this.attach(); } else { await this.detach(); } }, this.disposables); let provider = { onDidChange: null, provideTextDocumentContent: async (uri) => { let channel = this.outputChannels.get(uri.path.slice(1)); if (!channel) return ''; nvim.pauseNotification(); nvim.command('setlocal nospell nofoldenable wrap noswapfile', true); nvim.command('setlocal buftype=nofile bufhidden=hide', true); nvim.command('setfiletype log', true); await nvim.resumeNotification(); return channel.content; } }; this.disposables.push(this.registerTextDocumentContentProvider('output', provider)); } getConfigFile(target) { return this.configurations.getConfigFile(target); } /** * Register autocmd on vim. */ registerAutocmd(autocmd) { let id = this.autocmds.size + 1; this.autocmds.set(id, autocmd); this.setupDynamicAutocmd(); return vscode_languageserver_protocol_1.Disposable.create(() => { this.autocmds.delete(id); this.setupDynamicAutocmd(); }); } /** * Watch for option change. */ watchOption(key, callback, disposables) { let watching = this.watchedOptions.has(key); if (!watching) { this.watchedOptions.add(key); this.setupDynamicAutocmd(); } let disposable = events_1.default.on('OptionSet', async (changed, oldValue, newValue) => { if (changed == key && callback) { await Promise.resolve(callback(oldValue, newValue)); } }); if (disposables) { disposables.push(vscode_languageserver_protocol_1.Disposable.create(() => { disposable.dispose(); if (watching) return; this.watchedOptions.delete(key); this.setupDynamicAutocmd(); })); } } /** * Watch global variable, works on neovim only. */ watchGlobal(key, callback, disposables) { let { nvim } = this; nvim.call('coc#_watch', key, true); let disposable = events_1.default.on('GlobalChange', async (changed, oldValue, newValue) => { if (changed == key && callback) { await Promise.resolve(callback(oldValue, newValue)); } }); if (disposables) { disposables.push(vscode_languageserver_protocol_1.Disposable.create(() => { disposable.dispose(); nvim.call('coc#_unwatch', key, true); })); } } get cwd() { return this._cwd; } get env() { return this._env; } get root() { return this._root || this.cwd; } get rootPath() { return this.root; } get workspaceFolders() { return this._workspaceFolders; } /** * uri of current file, could be null */ get uri() { let { bufnr } = this; if (bufnr) { let document = this.getDocument(bufnr); if (document && document.schema == 'file') { return document.uri; } } return null; } get workspaceFolder() { let { rootPath } = this; if (rootPath == os_1.default.homedir()) return null; return { uri: vscode_uri_1.URI.file(rootPath).toString(), name: path_1.default.basename(rootPath) }; } get textDocuments() { let docs = []; for (let b of this.buffers.values()) { docs.push(b.textDocument); } return docs; } get documents() { return Array.from(this.buffers.values()); } createNameSpace(name = '') { if (this.namespaceMap.has(name)) return this.namespaceMap.get(name); NAME_SPACE = NAME_SPACE + 1; this.namespaceMap.set(name, NAME_SPACE); return NAME_SPACE; } get channelNames() { return Array.from(this.outputChannels.keys()); } get pluginRoot() { return path_1.default.dirname(__dirname); } get isVim() { return this._env.isVim; } get isNvim() { return !this._env.isVim; } get completeOpt() { return this._env.completeOpt; } get initialized() { return this._initialized; } get ready() { if (this._initialized) return Promise.resolve(); return new Promise(resolve => { let disposable = this.onDidWorkspaceInitialized(() => { disposable.dispose(); resolve(); }); }); } /** * Current filetypes. */ get filetypes() { let res = new Set(); for (let doc of this.documents) { res.add(doc.filetype); } return res; } /** * Check if selector match document. */ match(selector, document) { return match_1.score(selector, document.uri, document.languageId); } /** * Findup for filename or filenames from current filepath or root. */ async findUp(filename) { let { cwd } = this; let filepath = await this.nvim.call('expand', '%:p'); filepath = path_1.default.normalize(filepath); let isFile = filepath && path_1.default.isAbsolute(filepath); if (isFile && !fs_2.isParentFolder(cwd, filepath)) { // can't use cwd return fs_2.findUp(filename, path_1.default.dirname(filepath)); } let res = fs_2.findUp(filename, cwd); if (res && res != os_1.default.homedir()) return res; if (isFile) return fs_2.findUp(filename, path_1.default.dirname(filepath)); return null; } async resolveRootFolder(uri, patterns) { let { cwd } = this; if (uri.scheme != 'file') return cwd; let filepath = path_1.default.normalize(uri.fsPath); let dir = path_1.default.dirname(filepath); return fs_2.resolveRoot(dir, patterns) || dir; } /** * Create a FileSystemWatcher instance, * doesn't fail when watchman not found. */ createFileSystemWatcher(globPattern, ignoreCreate, ignoreChange, ignoreDelete) { let watchmanPath = process.env.NODE_ENV == 'test' ? null : this.getWatchmanPath(); let channel = watchmanPath ? this.createOutputChannel('watchman') : null; let promise = watchmanPath ? watchman_1.default.createClient(watchmanPath, this.root, channel) : Promise.resolve(null); let watcher = new fileSystemWatcher_1.default(promise, globPattern, !!ignoreCreate, !!ignoreChange, !!ignoreDelete); return watcher; } getWatchmanPath() { const preferences = this.getConfiguration('coc.preferences'); let watchmanPath = preferences.get('watchmanPath', 'watchman'); try { return which_1.default.sync(watchmanPath); } catch (e) { return null; } } /** * Get configuration by section and optional resource uri. */ getConfiguration(section, resource) { return this.configurations.getConfiguration(section, resource); } /** * Get created document by uri or bufnr. */ getDocument(uri) { if (typeof uri === 'number') { return this.buffers.get(uri); } uri = vscode_uri_1.URI.parse(uri).toString(); for (let doc of this.buffers.values()) { if (doc && doc.uri === uri) return doc; } return null; } /** * Get current cursor offset in document. */ async getOffset() { let document = await this.document; let pos = await this.getCursorPosition(); return document.textDocument.offsetAt(pos); } /** * Apply WorkspaceEdit. */ async applyEdit(edit) { let { nvim } = this; let { documentChanges, changes } = edit; if (documentChanges) { documentChanges = this.mergeDocumentChanges(documentChanges); if (!this.validteDocumentChanges(documentChanges)) return false; } let pos = await this.getCursorPosition(); let bufnr = await nvim.eval('bufnr("%")'); let currUri = this.getDocument(bufnr) ? this.getDocument(bufnr).uri : null; let changed = null; try { if (documentChanges && documentChanges.length) { let n = documentChanges.length; for (let change of documentChanges) { if (index_1.isDocumentEdit(change)) { let { textDocument, edits } = change; if (vscode_uri_1.URI.parse(textDocument.uri).toString() == currUri) { changed = position_1.getChangedFromEdits(pos, edits); } let doc = await this.loadFile(textDocument.uri); await doc.applyEdits(nvim, edits); } else if (vscode_languageserver_protocol_1.CreateFile.is(change)) { let file = vscode_uri_1.URI.parse(change.uri).fsPath; await this.createFile(file, change.options); } else if (vscode_languageserver_protocol_1.RenameFile.is(change)) { await this.renameFile(vscode_uri_1.URI.parse(change.oldUri).fsPath, vscode_uri_1.URI.parse(change.newUri).fsPath, change.options); } else if (vscode_languageserver_protocol_1.DeleteFile.is(change)) { await this.deleteFile(vscode_uri_1.URI.parse(change.uri).fsPath, change.options); } } this.showMessage(`${n} buffers changed.`); } else if (changes) { for (let uri of Object.keys(changes)) { let document = await this.loadFile(uri); if (vscode_uri_1.URI.parse(uri).toString() == currUri) { changed = position_1.getChangedFromEdits(pos, changes[uri]); } await document.applyEdits(nvim, changes[uri]); } this.showMessage(`${Object.keys(changes).length} buffers changed.`); } if (changed) { pos.line = pos.line + changed.line; pos.character = pos.character + changed.character; } await this.moveTo(pos); } catch (e) { // await nvim.setOption('eventignore', origIgnore) this.showMessage(`Error on applyEdits: ${e}`, 'error'); return false; } return true; } /** * Convert location to quickfix item. */ async getQuickfixItem(loc, text, type = '') { if (vscode_languageserver_protocol_1.LocationLink.is(loc)) { loc = vscode_languageserver_protocol_1.Location.create(loc.targetUri, loc.targetRange); } let doc = this.getDocument(loc.uri); let { uri, range } = loc; let { line, character } = range.start; let u = vscode_uri_1.URI.parse(uri); let bufnr = doc ? doc.bufnr : -1; if (!text && u.scheme == 'file') { text = await this.getLine(uri, line); character = string_1.byteIndex(text, character); } let item = { uri, filename: u.scheme == 'file' ? u.fsPath : uri, lnum: line + 1, col: character + 1, text: text || '', range }; if (type) item.type = type; if (bufnr != -1) item.bufnr = bufnr; return item; } /** * Create persistence Mru instance. */ createMru(name) { return new mru_1.default(name); } async getSelectedRange(mode, document) { let { nvim } = this; if (['v', 'V', 'char', 'line'].indexOf(mode) == -1) { this.showMessage(`Mode '${mode}' is not supported`, 'error'); return null; } let isVisual = ['v', 'V'].indexOf(mode) != -1; let [, sl, sc] = await nvim.call('getpos', isVisual ? `'<` : `'[`); let [, el, ec] = await nvim.call('getpos', isVisual ? `'>` : `']`); let range = vscode_languageserver_protocol_1.Range.create(document.getPosition(sl, sc), document.getPosition(el, ec)); if (mode == 'v') { range.end.character = range.end.character + 1; } return range; } /** * Visual select range of current document */ async selectRange(range) { let { nvim } = this; let { start, end } = range; let [bufnr, ve, selection, mode] = await nvim.eval(`[bufnr('%'), &virtualedit, &selection, mode()]`); let document = this.getDocument(bufnr); if (!document) return; let line = document.getline(start.line); let col = line ? string_1.byteLength(line.slice(0, start.character)) : 0; let endLine = document.getline(end.line); let endCol = endLine ? string_1.byteLength(endLine.slice(0, end.character)) : 0; let move_cmd = ''; let resetVirtualEdit = false; // if (mode != 'n') move_cmd += "\\<Esc>" move_cmd += 'v'; endCol = await nvim.eval(`virtcol([${end.line + 1}, ${endCol}])`); if (selection == 'inclusive') { if (end.character == 0) { move_cmd += `${end.line}G`; } else { move_cmd += `${end.line + 1}G${endCol}|`; } } else if (selection == 'old') { move_cmd += `${end.line + 1}G${endCol}|`; } else { move_cmd += `${end.line + 1}G${endCol + 1}|`; } col = await nvim.eval(`virtcol([${start.line + 1}, ${col}])`); move_cmd += `o${start.line + 1}G${col + 1}|o`; nvim.pauseNotification(); if (ve != 'onemore') { resetVirtualEdit = true; nvim.setOption('virtualedit', 'onemore', true); } nvim.command(`noa call cursor(${start.line + 1},${col + (move_cmd == 'a' ? 0 : 1)})`, true); // nvim.call('eval', [`feedkeys("${move_cmd}", 'in')`], true) nvim.command(`normal! ${move_cmd}`, true); if (resetVirtualEdit) nvim.setOption('virtualedit', ve, true); if (this.isVim) nvim.command('redraw', true); await nvim.resumeNotification(); } /** * Populate locations to UI. */ async showLocations(locations) { let items = await Promise.all(locations.map(loc => { return this.getQuickfixItem(loc); })); let { nvim } = this; const preferences = this.getConfiguration('coc.preferences'); if (preferences.get('useQuickfixForLocations', false)) { await nvim.call('setqflist', [items]); nvim.command('copen', true); } else { await nvim.setVar('coc_jump_locations', items); if (this.env.locationlist) { nvim.command('CocList --normal --auto-preview location', true); } else { nvim.command('doautocmd User CocLocationsChange', true); } } } /** * Get content of line by uri and line. */ async getLine(uri, line) { let document = this.getDocument(uri); if (document) return document.getline(line) || ''; if (!uri.startsWith('file:')) return ''; return await fs_2.readFileLine(vscode_uri_1.URI.parse(uri).fsPath, line); } /** * Get WorkspaceFolder of uri */ getWorkspaceFolder(uri) { this.workspaceFolders.sort((a, b) => b.uri.length - a.uri.length); let filepath = vscode_uri_1.URI.parse(uri).fsPath; return this.workspaceFolders.find(folder => fs_2.isParentFolder(vscode_uri_1.URI.parse(folder.uri).fsPath, filepath)); } /** * Get content from buffer of file by uri. */ async readFile(uri) { let document = this.getDocument(uri); if (document) { document.forceSync(); return document.content; } let u = vscode_uri_1.URI.parse(uri); if (u.scheme != 'file') return ''; let encoding = await this.getFileEncoding(); return await fs_2.readFile(u.fsPath, encoding); } getFilepath(filepath) { let { cwd } = this; let rel = path_1.default.relative(cwd, filepath); return rel.startsWith('..') ? filepath : rel; } onWillSaveUntil(callback, thisArg, clientId) { return this.willSaveUntilHandler.addCallback(callback, thisArg, clientId); } /** * Echo lines. */ async echoLines(lines, truncate = false) { let { nvim } = this; let cmdHeight = this.env.cmdheight; if (lines.length > cmdHeight && truncate) { lines = lines.slice(0, cmdHeight); } let maxLen = this.env.columns - 12; lines = lines.map(line => { line = line.replace(/\n/g, ' '); if (truncate) line = line.slice(0, maxLen); return line; }); if (truncate && lines.length == cmdHeight) { let last = lines[lines.length - 1]; lines[cmdHeight - 1] = `${last.length == maxLen ? last.slice(0, -4) : last} ...`; } nvim.callTimer('coc#util#echo_lines', [lines], true); } /** * Show message in vim. */ showMessage(msg, identify = 'more') { if (this._blocking || !this.nvim) return; let { messageLevel } = this; let level = types_1.MessageLevel.Error; let method = index_1.echoErr; switch (identify) { case 'more': level = types_1.MessageLevel.More; method = index_1.echoMessage; break; case 'warning': level = types_1.MessageLevel.Warning; method = index_1.echoWarning; break; } if (level >= messageLevel) { method(this.nvim, msg); } } /** * Current document. */ get document() { let { bufnr } = this; if (bufnr == null) return null; if (this.buffers.has(bufnr)) { return Promise.resolve(this.buffers.get(bufnr)); } if (!this.creatingSources.has(bufnr)) { this.onBufCreate(bufnr).catch(e => { logger.error('Error on buffer create:', e); }); } return new Promise(resolve => { let disposable = this.onDidOpenTextDocument(doc => { disposable.dispose(); resolve(this.getDocument(doc.uri)); }); }); } /** * Get current cursor position. */ async getCursorPosition() { let [line, character] = await this.nvim.call('coc#util#cursor'); return vscode_languageserver_protocol_1.Position.create(line, character); } /** * Get current document and position. */ async getCurrentState() { let document = await this.document; let position = await this.getCursorPosition(); return { document: document.textDocument, position }; } /** * Get format options */ async getFormatOptions(uri) { let doc; if (uri) { doc = this.getDocument(uri); } else { doc = this.getDocument(this.bufnr); } let tabSize = await this.getDocumentOption('shiftwidth', doc); if (!tabSize) tabSize = await this.getDocumentOption('tabstop', doc); let insertSpaces = (await this.getDocumentOption('expandtab', doc)) == 1; return { tabSize, insertSpaces }; } /** * Jump to location. */ async jumpTo(uri, position, openCommand) { const preferences = this.getConfiguration('coc.preferences'); let jumpCommand = openCommand || preferences.get('jumpCommand', 'edit'); let { nvim } = this; let { line, character } = position || { line: 0, character: 0 }; let doc = this.getDocument(uri); let col = character + 1; if (doc) col = string_1.byteLength(doc.getline(line).slice(0, character)) + 1; let bufnr = doc ? doc.bufnr : -1; await nvim.command(`normal! m'`); if (bufnr == this.bufnr && position && jumpCommand == 'edit') { await nvim.call('cursor', [line + 1, col]); } else if (bufnr != -1 && jumpCommand == 'edit') { let moveCmd = position ? `+call\\ cursor(${line + 1},${col})` : ''; await this.nvim.call('coc#util#execute', [`buffer ${moveCmd} ${bufnr}`]); } else { let bufname = uri.startsWith('file:') ? path_1.default.normalize(vscode_uri_1.URI.parse(uri).fsPath) : uri; let pos = position ? [line + 1, col] : []; await this.nvim.call('coc#util#jump', [jumpCommand, bufname, pos]); } } /** * Move cursor to position. */ async moveTo(position) { await this.nvim.call('coc#util#jumpTo', [position.line, position.character]); } /** * Create a file in vim and disk */ async createFile(filepath, opts = {}) { let stat = await fs_2.statAsync(filepath); if (stat && !opts.overwrite && !opts.ignoreIfExists) { this.showMessage(`${filepath} already exists!`, 'error'); return; } if (!stat || opts.overwrite) { // directory if (filepath.endsWith('/')) { try { if (filepath.startsWith('~')) filepath = filepath.replace(/^~/, os_1.default.homedir()); await index_1.mkdirp(filepath); } catch (e) { this.showMessage(`Can't create ${filepath}: ${e.message}`, 'error'); } } else { let uri = vscode_uri_1.URI.file(filepath).toString(); let doc = this.getDocument(uri); if (doc) return; let encoding = await this.getFileEncoding(); fs_1.default.writeFileSync(filepath, '', encoding || ''); await this.loadFile(uri); } } } /** * Load uri as document. */ async loadFile(uri) { let u = vscode_uri_1.URI.parse(uri); let doc = this.getDocument(uri); if (doc) return doc; let { nvim } = this; let filepath = u.scheme == 'file' ? u.fsPath : uri; let escaped = await nvim.call('fnameescape', filepath); let bufnr = await nvim.call('bufnr', '%'); nvim.pauseNotification(); nvim.command('setl bufhidden=hide', true); nvim.command(`keepalt edit ${escaped}`, true); nvim.command('setl bufhidden=hide', true); nvim.command(`keepalt buffer ${bufnr}`, true); return await new Promise((resolve, reject) => { let disposable = this.onDidOpenTextDocument(textDocument => { if (textDocument.uri == uri) { clearTimeout(timer); disposable.dispose(); resolve(this.getDocument(uri)); } }); let timer = setTimeout(() => { disposable.dispose(); reject(new Error(`Create document ${uri} timeout after 1s.`)); }, 1000); nvim.resumeNotification(false, true).catch(_e => { // noop }); }); } /** * Rename file in vim and disk */ async renameFile(oldPath, newPath, opts = {}) { let { overwrite, ignoreIfExists } = opts; let stat = await fs_2.statAsync(newPath); if (stat && !overwrite && !ignoreIfExists) { this.showMessage(`${newPath} already exists`, 'error'); return; } if (!stat || overwrite) { try { await fs_2.renameAsync(oldPath, newPath); let uri = vscode_uri_1.URI.file(oldPath).toString(); let doc = this.getDocument(uri); if (doc) { await doc.buffer.setName(newPath); // avoid cancel by unload await this.onBufCreate(doc.bufnr); } } catch (e) { this.showMessage(`Rename error ${e.message}`, 'error'); } } } /** * Delete file from vim and disk. */ async deleteFile(filepath, opts = {}) { let { ignoreIfNotExists, recursive } = opts; let stat = await fs_2.statAsync(filepath.replace(/\/$/, '')); let isDir = stat && stat.isDirectory() || filepath.endsWith('/'); if (!stat && !ignoreIfNotExists) { this.showMessage(`${filepath} not exists`, 'error'); return; } if (stat == null) return; if (isDir && !recursive) { this.showMessage(`Can't remove directory, recursive not set`, 'error'); return; } try { let method = isDir ? 'rmdir' : 'unlink'; await util_1.default.promisify(fs_1.default[method])(filepath); if (!isDir) { let uri = vscode_uri_1.URI.file(filepath).toString(); let doc = this.getDocument(uri); if (doc) await this.nvim.command(`silent bwipeout ${doc.bufnr}`); } } catch (e) { this.showMessage(`Error on delete ${filepath}: ${e.message}`, 'error'); } } /** * Open resource by uri */ async openResource(uri) { let { nvim } = this; // not supported if (uri.startsWith('http')) { await nvim.call('coc#util#open_url', uri); return; } let wildignore = await nvim.getOption('wildignore'); await nvim.setOption('wildignore', ''); await this.jumpTo(uri); await nvim.setOption('wildignore', wildignore); } /** * Create a new output channel */ createOutputChannel(name) { if (this.outputChannels.has(name)) return this.outputChannels.get(name); let channel = new outputChannel_1.default(name, this.nvim); this.outputChannels.set(name, channel); return channel; } /** * Reveal buffer of output channel. */ showOutputChannel(name, preserveFocus) { let channel = this.outputChannels.get(name); if (!channel) { this.showMessage(`Channel "${name}" not found`, 'error'); return; } channel.show(preserveFocus); } /** * Resovle module from yarn or npm. */ async resolveModule(name) { return await this.resolver.resolveModule(name); } /** * Run nodejs command */ async runCommand(cmd, cwd, timeout) { cwd = cwd || this.cwd; return index_1.runCommand(cmd, { cwd }, timeout); } /** * Run command in vim terminal */ async runTerminalCommand(cmd, cwd = this.cwd, keepfocus = false) { return await this.nvim.callAsync('coc#util#run_terminal', { cmd, cwd, keepfocus: keepfocus ? 1 : 0 }); } async createTerminal(opts) { let cmd = opts.shellPath; let args = opts.shellArgs; if (!cmd) cmd = await this.nvim.getOption('shell'); let terminal = new terminal_1.default(cmd, args || [], this.nvim, opts.name); await terminal.start(opts.cwd || this.cwd, opts.env); this.terminals.set(terminal.bufnr, terminal); this._onDidOpenTerminal.fire(terminal); return terminal; } /** * Show quickpick */ async showQuickpick(items, placeholder = 'Choose by number') { let msgs = [placeholder + ':']; msgs = msgs.concat(items.map((str, index) => { return `${index + 1}. ${str}`; })); let res = await this.callAsync('inputlist', [msgs]); let n = parseInt(res, 10); if (isNaN(n) || n <= 0 || n > msgs.length) return -1; return n - 1; } /** * Prompt for confirm action. */ async showPrompt(title) { this._blocking = true; let res = await this.nvim.callAsync('coc#util#with_callback', ['coc#util#prompt_confirm', [title]]); this._blocking = false; return res == 1; } async callAsync(method, args) { if (this.isNvim) return await this.nvim.call(method, args); return await this.nvim.callAsync('coc#util#with_callback', [method, args]); } /** * Request input from user */ async requestInput(title, defaultValue) { let { nvim } = this; let res = await this.callAsync('input', [title + ':', defaultValue || '']); nvim.command('normal! :<C-u>', true); if (!res) { this.showMessage('Empty word, canceled', 'warning'); return null; } return res; } /** * registerTextDocumentContentProvider */ registerTextDocumentContentProvider(scheme, provider) { this.schemeProviderMap.set(scheme, provider); this.setupDynamicAutocmd(); // tslint:disable-line let disposables = []; if (provider.onDidChange) { provider.onDidChange(async (uri) => { let doc = this.getDocument(uri.toString()); if (doc) { let { buffer } = doc; let tokenSource = new vscode_languageserver_protocol_1.CancellationTokenSource(); let content = await Promise.resolve(provider.provideTextDocumentContent(uri, tokenSource.token)); await buffer.setLines(content.split('\n'), { start: 0, end: -1, strictIndexing: false }); } }, null, disposables); } return vscode_languageserver_protocol_1.Disposable.create(() => { this.schemeProviderMap.delete(scheme); index_1.disposeAll(disposables); this.setupDynamicAutocmd(); }); } /** * Register keymap */ registerKeymap(modes, key, fn, opts = {}) { if (this.keymaps.has(key)) return; opts = Object.assign({ sync: true, cancel: true, silent: true, repeat: false }, opts); let { nvim } = this; this.keymaps.set(key, [fn, !!opts.repeat]); let method = opts.sync ? 'request' : 'notify'; let silent = opts.silent ? '<silent>' : ''; for (let m of modes) { if (m == 'i') { nvim.command(`inoremap ${silent}<expr> <Plug>(coc-${key}) coc#_insert_key('${method}', '${key}', ${opts.cancel ? 1 : 0})`, true); } else { let modify = this.isNvim ? '<Cmd>' : index_1.getKeymapModifier(m); nvim.command(`${m}noremap ${silent} <Plug>(coc-${key}) ${modify}:call coc#rpc#${method}('doKeymap', ['${key}'])<cr>`, true); } } return vscode_languageserver_protocol_1.Disposable.create(() => { this.keymaps.delete(key); for (let m of modes) { nvim.command(`${m}unmap <Plug>(coc-${key})`, true); } }); } /** * Register expr keymap. */ registerExprKeymap(mode, key, fn, buffer = false) { let id = uuid(); let { nvim } = this; this.keymaps.set(id, [fn, false]); if (mode == 'i') { nvim.command(`inoremap <silent><expr>${buffer ? '<nowait><buffer>' : ''} ${key} coc#_insert_key('request', '${id}')`, true); } else { nvim.command(`${mode}noremap <silent><expr>${buffer ? '<nowait><buffer>' : ''} ${key} coc#rpc#request('doKeymap', ['${id}'])`, true); } return vscode_languageserver_protocol_1.Disposable.create(() => { this.keymaps.delete(id); nvim.command(`${mode}unmap ${buffer ? '<buffer>' : ''} ${key}`, true); }); } registerLocalKeymap(mode, key, fn, notify = false) { let id = uuid(); let { nvim } = this; this.keymaps.set(id, [fn, false]); nvim.command(`${mode}noremap <silent><nowait><buffer> ${key} :call coc#rpc#${notify ? 'notify' : 'request'}('doKeymap', ['${id}'])<CR>`, true); return vscode_languageserver_protocol_1.Disposable.create(() => { this.keymaps.delete(id); nvim.command(`${mode}unmap <buffer> ${key}`, true); }); } /** * Create StatusBarItem */ createStatusBarItem(priority = 0, opt = {}) { if (!this.statusLine) { // tslint:disable-next-line: no-empty let fn = () => { }; return { text: '', show: fn, dispose: fn, hide: fn, priority: 0, isProgress: true }; } return this.statusLine.createStatusBarItem(priority, opt.progress || false); } dispose() { this._disposed = true; for (let ch of this.outputChannels.values()) { ch.dispose(); } for (let doc of this.documents) { doc.detach(); } index_1.disposeAll(this.disposables); watchman_1.default.dispose(); this.configurations.dispose(); this.setupDynamicAutocmd.clear(); this.buffers.clear(); if (this.statusLine) this.statusLine.dispose(); } async detach() { if (!this._attached) return; this._attached = false; for (let bufnr of this.buffers.keys()) { await events_1.default.fire('BufUnload', [bufnr]); } } /** * Create DB instance at extension root. */ createDatabase(name) { let root; if (global.hasOwnProperty('__TEST__')) { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'coc-')); } else { root = path_1.default.dirname(this.env.extensionRoot); } let filepath = path_1.default.join(root, name + '.json'); return new db_1.default(filepath); } /** * Create Task instance that runs in vim. */ createTask(id) { return new task_1.default(this.nvim, id); } async _setupDynamicAutocmd() { let schemes = this.schemeProviderMap.keys(); let cmds = []; for (let scheme of schemes) { cmds.push(`autocmd BufReadCmd,FileReadCmd,SourceCmd ${scheme}://* call coc#rpc#request('CocAutocmd', ['BufReadCmd','${scheme}', expand('<amatch>')])`); } for (let [id, autocmd] of this.autocmds.entries()) { let args = autocmd.arglist && autocmd.arglist.length ? ', ' + autocmd.arglist.join(', ') : ''; let event = Array.isArray(autocmd.event) ? autocmd.event.join(' ') : autocmd.event; let pattern = '*'; if (/\buser\b/i.test(event)) { pattern = ''; } cmds.push(`autocmd ${event} ${pattern} call coc#rpc#${autocmd.request ? 'request' : 'notify'}('doAutocmd', [${id}${args}])`); } for (let key of this.watchedOptions) { cmds.push(`autocmd OptionSet ${key} call coc#rpc#notify('OptionSet',[expand('<amatch>'), v:option_old, v:option_new])`); } let content = ` augroup coc_autocmd autocmd! ${cmds.join('\n')} augroup end`; try { let filepath = path_1.default.join(os_1.default.tmpdir(), `coc-${process.pid}.vim`); await fs_2.writeFile(filepath, content); await this.nvim.command(`source ${filepath}`); } catch (e) { this.showMessage(`Can't create tmp file: ${e.message}`, 'error'); } } async onBufReadCmd(scheme, uri) { let provider = this.schemeProviderMap.get(scheme); if (!provider) { this.showMessage(`Provider for ${scheme} not found`, 'error'); return; } let tokenSource = new vscode_languageserver_protocol_1.CancellationTokenSource(); let content = await Promise.resolve(provider.provideTextDocumentContent(vscode_uri_1.URI.parse(uri), tokenSource.token)); let buf = await this.nvim.buffer; await buf.setLines(content.split('\n'), { start: 0, end: -1, strictIndexing: false }); setTimeout(async () => { await events_1.default.fire('BufCreate', [buf.id]); }, 30); } async attach() { if (this._attached) return; this._attached = true; let buffers = await this.nvim.buffers; let bufnr = this.bufnr = await this.nvim.call('bufnr', '%'); await Promise.all(buffers.map(buf => { return this.onBufCreate(buf); })); if (!this._initialized) { this._onDidWorkspaceInitialized.fire(void 0); this._initialized = true; } await events_1.default.fire('BufEnter', [bufnr]); let winid = await this.nvim.call('win_getid'); await events_1.default.fire('BufWinEnter', [bufnr, winid]); } validteDocumentChanges(documentChanges) { if (!documentChanges) return true; for (let change of documentChanges) { if (index_1.isDocumentEdit(change)) { let { textDocument } = change; let { uri, version } = textDocument; let doc = this.getDocument(uri); if (version && !doc) { this.showMessage(`${uri} not opened.`, 'error'); return false; } if (version && doc.version != version) { this.showMessage(`${uri} changed before apply edit`, 'error'); return false; } if (!version && !doc) { if (!uri.startsWith('file')) { this.showMessage(`Can't apply edits to ${uri}.`, 'error'); return false; } let exists = fs_1.default.existsSync(vscode_uri_1.URI.parse(uri).fsPath); if (!exists) { this.showMessage(`File ${uri} not exists.`, 'error'); return false; } } } else if (vscode_languageserver_protocol_1.CreateFile.is(change) || vscode_languageserver_protocol_1.DeleteFile.is(change)) { if (!fs_2.isFile(change.uri)) { this.showMessage(`Chagne of scheme ${change.uri} not supported`, 'error'); return false; } } } return true; } createConfigurations() { let home = process.env.VIMCONFIG || path_1.default.join(os_1.default.homedir(), '.vim'); if (global.hasOwnProperty('__TEST__')) { home = path_1.default.join(this.pluginRoot, 'src/__tests__'); } let userConfigFile = path_1.default.join(home, CONFIG_FILE_NAME); return new configuration_1.default(userConfigFile, new shape_1.default(this)); } // events for sync buffer of vim initVimEvents() { if (!this.isVim) return; const onChange = async (bufnr) => { let doc = this.getDocument(bufnr); if (doc && doc.shouldAttach) doc.fetchContent(); }; events_1.default.on('TextChangedI', onChange, null, this.disposables); events_1.default.on('TextChanged', onChange, null, this.disposables); } async onBufCreate(buf) { let buffer = typeof buf === 'number' ? this.nvim.createBuffer(buf) : buf; let bufnr = buffer.id; if (this.creatingSources.has(bufnr)) return; let document = this.getDocument(bufnr); try { if (document) this.onBufUnload(bufnr, true); document = new document_1.default(buffer, this._env); let source = new vscode_languageserver_protocol_1.CancellationTokenSource(); let token = source.token; this.creatingSources.set(bufnr, source); let created = await document.init(this.nvim, token); if (!created || document.getVar('enabled', 1) === 0) document = null; if (this.creatingSources.get(bufnr) == source) { source.dispose(); this.creatingSources.delete(bufnr); } } catch (e) { logger.error('Error on create buffer:', e); } if (!document) return; this.buffers.set(bufnr, document); document.onDocumentDetach(uri => {