UNPKG

coc.nvim

Version:

LSP based intellisense engine for neovim & vim8.

855 lines 33.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const debounce_1 = require("debounce"); const fast_diff_1 = tslib_1.__importDefault(require("fast-diff")); const fs_1 = tslib_1.__importDefault(require("fs")); const isuri_1 = tslib_1.__importDefault(require("isuri")); const path_1 = tslib_1.__importDefault(require("path")); const rimraf_1 = tslib_1.__importDefault(require("rimraf")); const semver_1 = tslib_1.__importDefault(require("semver")); const util_1 = tslib_1.__importStar(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 commands_1 = tslib_1.__importDefault(require("./commands")); const events_1 = tslib_1.__importDefault(require("./events")); const db_1 = tslib_1.__importDefault(require("./model/db")); const extension_1 = tslib_1.__importDefault(require("./model/extension")); const memos_1 = tslib_1.__importDefault(require("./model/memos")); const util_2 = require("./util"); const mkdirp_1 = tslib_1.__importDefault(require("mkdirp")); const array_1 = require("./util/array"); require("./util/extensions"); const factory_1 = require("./util/factory"); const fs_2 = require("./util/fs"); const watchman_1 = tslib_1.__importDefault(require("./watchman")); const workspace_1 = tslib_1.__importDefault(require("./workspace")); const createLogger = require('./util/logger'); const logger = createLogger('extensions'); function loadJson(file) { try { let content = fs_1.default.readFileSync(file, 'utf8'); return JSON.parse(content); } catch (e) { return null; } } class Extensions { constructor() { this.list = []; this.disabled = new Set(); this._onDidLoadExtension = new vscode_languageserver_protocol_1.Emitter(); this._onDidActiveExtension = new vscode_languageserver_protocol_1.Emitter(); this._onDidUnloadExtension = new vscode_languageserver_protocol_1.Emitter(); this._additionalSchemes = {}; this.activated = false; this.ready = true; this.onDidLoadExtension = this._onDidLoadExtension.event; this.onDidActiveExtension = this._onDidActiveExtension.event; this.onDidUnloadExtension = this._onDidUnloadExtension.event; } async init(nvim) { if (global.hasOwnProperty('__TEST__')) { this.root = path_1.default.join(__dirname, './__tests__/extensions'); this.manager = new extension_1.default(this.root); let filepath = path_1.default.join(this.root, 'db.json'); let db = this.db = new db_1.default(filepath); } else { await this.initializeRoot(); } let data = loadJson(this.db.filepath) || {}; let keys = Object.keys(data.extension || {}); for (let key of keys) { if (data.extension[key].disabled == true) { this.disabled.add(key); } } if (process.env.COC_NO_PLUGINS) return; let stats = await this.globalExtensionStats(); let localStats = await this.localExtensionStats(stats); stats = stats.concat(localStats); this.memos = new memos_1.default(path_1.default.resolve(this.root, '../memos.json')); await this.loadFileExtensions(); await Promise.all(stats.map(stat => { return this.loadExtension(stat.root, stat.isLocal).catch(e => { workspace_1.default.showMessage(`Can't load extension from ${stat.root}: ${e.message}'`, 'error'); }); })); // watch for new local extension workspace_1.default.watchOption('runtimepath', async (oldValue, newValue) => { let result = fast_diff_1.default(oldValue, newValue); for (let [changeType, value] of result) { if (changeType == 1) { let paths = value.replace(/,$/, '').split(','); for (let p of paths) { await this.loadExtension(p, true); } } } }); commands_1.default.register({ id: 'extensions.forceUpdateAll', execute: async () => { await this.cleanExtensions(); await this.installExtensions([]); } }); } async activateExtensions() { this.activated = true; if (global.hasOwnProperty('__TEST__')) return; for (let item of this.list) { let { id, packageJSON } = item.extension; this.setupActiveEvents(id, packageJSON); } // check extensions need watch & install this.checkExtensions().logError(); let config = workspace_1.default.getConfiguration('coc.preferences'); let interval = config.get('extensionUpdateCheck', 'daily'); if (interval != 'never') { let now = new Date(); let day = new Date(now.getFullYear(), now.getMonth(), now.getDate() - (interval == 'daily' ? 0 : 7)); let ts = this.db.fetch('lastUpdate'); if (ts && Number(ts) > day.getTime()) return; this.updateExtensions().logError(); } } async updateExtensions() { if (!this.root) await this.initializeRoot(); if (!this.npm) return; let lockedList = await this.getLockedList(); let stats = await this.globalExtensionStats(); let versionInfo = {}; stats = stats.filter(o => !this.disabled.has(o.id) && !lockedList.includes(o.id)); let names = stats.map(o => o.id); let statusItem = workspace_1.default.createStatusBarItem(0, { progress: true }); statusItem.text = `Updating extensions.`; statusItem.show(); await Promise.all(names.map(name => { let o = stats.find(o => o.id == name); return this.manager.update(this.npm, name, o.exotic ? o.uri : undefined).then(updated => { if (updated) this.reloadExtension(name).logError(); }, err => { workspace_1.default.showMessage(`Error on update ${name}: ${err}`); }); })); this.db.push('lastUpdate', Date.now()); workspace_1.default.showMessage('Update completed', 'more'); statusItem.dispose(); } async checkExtensions() { let { globalExtensions, watchExtensions } = workspace_1.default.env; if (globalExtensions && globalExtensions.length) { let names = globalExtensions.filter(name => !this.isDisabled(name)); let folder = path_1.default.join(this.root, 'node_modules'); if (fs_1.default.existsSync(folder)) { let files = await util_1.default.promisify(fs_1.default.readdir)(folder); names = names.filter(s => files.indexOf(s) == -1); } let json = this.loadJson(); if (json && json.dependencies) { let vals = Object.values(json.dependencies); names = names.filter(s => vals.findIndex(val => val.indexOf(s) !== -1) == -1); } this.installExtensions(names).logError(); } // watch for changes if (watchExtensions && watchExtensions.length) { let watchmanPath = workspace_1.default.getWatchmanPath(); if (!watchmanPath) return; let stats = await this.getExtensionStates(); for (let name of watchExtensions) { let stat = stats.find(s => s.id == name); if (stat && stat.state !== 'disabled') { let directory = await util_1.default.promisify(fs_1.default.realpath)(stat.root); let client = await watchman_1.default.createClient(watchmanPath, directory); client.subscribe('**/*.js', debounce_1.debounce(async () => { await this.reloadExtension(name); workspace_1.default.showMessage(`reloaded ${name}`); }, 100)).catch(_e => { // noop }); } } } } /** * Install extensions, can be called without initialize. */ async installExtensions(list = []) { let { npm } = this; if (!npm) return; if (!this.root) await this.initializeRoot(); let missing = this.getMissingExtensions(); if (missing.length) list.push(...missing); if (!list.length) return; list = array_1.distinct(list); let statusItem = workspace_1.default.createStatusBarItem(0, { progress: true }); statusItem.show(); statusItem.text = `Installing ${list.join(' ')}`; await Promise.all(list.map(def => { return this.manager.install(npm, def).then(name => { if (name) this.onExtensionInstall(name).logError(); }, err => { workspace_1.default.showMessage(`Error on install ${def}: ${err}`); }); })); statusItem.dispose(); } /** * Get list of extensions in package.json that not installed */ getMissingExtensions() { let json = this.loadJson() || { dependencies: {} }; let ids = []; for (let key of Object.keys(json.dependencies)) { let folder = path_1.default.join(this.root, 'node_modules', key); if (!fs_1.default.existsSync(folder)) { let val = json.dependencies[key]; if (val.startsWith('http')) { ids.push(val); } else { ids.push(key); } } } return ids; } get npm() { let npm = workspace_1.default.getConfiguration('npm').get('binPath', 'npm'); for (let exe of [npm, 'yarnpkg', 'yarn', 'npm']) { try { let res = which_1.default.sync(exe); return res; } catch (e) { continue; } } workspace_1.default.showMessage(`Can't find npm or yarn in your $PATH`, 'error'); return null; } /** * Get all loaded extensions. */ get all() { return this.list.map(o => o.extension); } getExtension(id) { return this.list.find(o => o.id == id); } getExtensionState(id) { let disabled = this.isDisabled(id); if (disabled) return 'disabled'; let item = this.list.find(o => o.id == id); if (!item) return 'unknown'; let { extension } = item; return extension.isActive ? 'activated' : 'loaded'; } async getExtensionStates() { let globalStats = await this.globalExtensionStats(); let localStats = await this.localExtensionStats(globalStats); return globalStats.concat(localStats); } async getLockedList() { let obj = await this.db.fetch('extension'); obj = obj || {}; return Object.keys(obj).filter(id => { return obj[id].locked === true; }); } async toggleLock(id) { let key = `extension.${id}.locked`; let locked = await this.db.fetch(key); if (locked) { this.db.delete(key); } else { this.db.push(key, true); } } async toggleExtension(id) { let state = this.getExtensionState(id); if (state == null) return; if (state == 'activated') { this.deactivate(id); } let key = `extension.${id}.disabled`; this.db.push(key, state == 'disabled' ? false : true); if (state != 'disabled') { this.disabled.add(id); // unload let idx = this.list.findIndex(o => o.id == id); this.list.splice(idx, 1); } else { this.disabled.delete(id); let p = global.hasOwnProperty('__TEST__') ? '' : 'node_modules'; let folder = path_1.default.join(this.root, p, id); try { await this.loadExtension(folder); } catch (e) { workspace_1.default.showMessage(`Can't load extension ${id}: ${e.message}'`, 'error'); } } await util_2.wait(200); } async reloadExtension(id) { let idx = this.list.findIndex(o => o.id == id); let directory = idx == -1 ? null : this.list[idx].directory; this.deactivate(id); if (idx != -1) this.list.splice(idx, 1); await util_2.wait(200); if (directory) { await this.loadExtension(directory); } else { this.activate(id); } } /** * Remove all installed extensions */ async cleanExtensions() { let dir = path_1.default.join(this.root, 'node_modules'); if (!fs_1.default.existsSync(dir)) return; let names = fs_1.default.readdirSync(dir); for (let name of names) { let file = path_1.default.join(dir, name); let stat = await util_1.promisify(fs_1.default.lstat)(file); if (stat.isSymbolicLink()) continue; await util_1.promisify(rimraf_1.default)(file, { glob: false }); } } async uninstallExtension(ids) { if (!ids.length) return; let status = workspace_1.default.createStatusBarItem(99, { progress: true }); try { status.text = `Uninstalling ${ids.join(' ')}`; status.show(); let removed = []; for (let id of ids) { if (!this.isGlobalExtension(id)) { workspace_1.default.showMessage(`Global extension '${id}' not found.`, 'error'); continue; } this.deactivate(id); removed.push(id); } for (let id of removed) { let idx = this.list.findIndex(o => o.id == id); if (idx != -1) { this.list.splice(idx, 1); this._onDidUnloadExtension.fire(id); } } let json = this.loadJson() || { dependencies: {} }; for (let id of removed) { delete json.dependencies[id]; let folder = path_1.default.join(this.root, 'node_modules', id); if (fs_1.default.existsSync(folder)) { await util_1.default.promisify(rimraf_1.default)(`${folder}`, { glob: false }); } } let jsonFile = path_1.default.join(this.root, 'package.json'); status.dispose(); fs_1.default.writeFileSync(jsonFile, JSON.stringify(json, null, 2), { encoding: 'utf8' }); workspace_1.default.showMessage(`Removed: ${ids.join(' ')}`); } catch (e) { status.dispose(); workspace_1.default.showMessage(`Uninstall failed: ${e.message}`, 'error'); } } isDisabled(id) { return this.disabled.has(id); } async onExtensionInstall(id) { if (!id) return; let item = this.list.find(o => o.id == id); if (item) item.deactivate(); let folder = path_1.default.join(this.root, 'node_modules', id); let stat = await fs_2.statAsync(folder); if (stat && stat.isDirectory()) { let jsonFile = path_1.default.join(folder, 'package.json'); let content = await fs_2.readFile(jsonFile, 'utf8'); let packageJSON = JSON.parse(content); let { engines } = packageJSON; if (!engines || (!engines.hasOwnProperty('coc') && !engines.hasOwnProperty('vscode'))) return; await this.loadExtension(folder); } } has(id) { return this.list.find(o => o.id == id) != null; } isActivted(id) { let item = this.list.find(o => o.id == id); if (item && item.extension.isActive) { return true; } return false; } async loadExtension(folder, isLocal = false) { let jsonFile = path_1.default.join(folder, 'package.json'); let stat = await fs_2.statAsync(jsonFile); if (!stat || !stat.isFile()) return; let content = await fs_2.readFile(jsonFile, 'utf8'); let packageJSON = JSON.parse(content); if (this.isDisabled(packageJSON.name)) return; if (this.isActivted(packageJSON.name)) { workspace_1.default.showMessage(`deactivate ${packageJSON.name}`); this.deactivate(packageJSON.name); await util_2.wait(200); } let { engines } = packageJSON; if (engines && engines.hasOwnProperty('coc')) { let required = engines.coc.replace(/^\^/, '>='); if (!semver_1.default.satisfies(workspace_1.default.version, required)) { workspace_1.default.showMessage(`Please update coc.nvim, ${packageJSON.name} requires coc.nvim ${engines.coc}`, 'warning'); } this.createExtension(folder, Object.freeze(packageJSON), isLocal); } else if (engines && engines.hasOwnProperty('vscode')) { this.createExtension(folder, Object.freeze(packageJSON), isLocal); } else { logger.info(`engine coc & vscode not found in ${jsonFile}`); } } async loadFileExtensions() { if (!process.env.VIMCONFIG) return; let folder = path_1.default.join(process.env.VIMCONFIG, 'coc-extensions'); if (!fs_1.default.existsSync(folder)) return; let files = await fs_2.readdirAsync(folder); files = files.filter(f => f.endsWith('.js')); for (let file of files) { this.loadExtensionFile(path_1.default.join(folder, file)); } } /** * Load single javascript file as extension. */ loadExtensionFile(filepath) { let filename = path_1.default.basename(filepath); let name = path_1.default.basename(filepath, 'js'); if (this.isDisabled(name)) return; let root = path_1.default.dirname(filepath); let packageJSON = { name, main: filename, }; this.createExtension(root, packageJSON); } activate(id, silent = true) { if (this.isDisabled(id)) { if (!silent) workspace_1.default.showMessage(`Extension ${id} is disabled!`, 'error'); return; } let item = this.list.find(o => o.id == id); if (!item) { workspace_1.default.showMessage(`Extension ${id} not found!`, 'error'); return; } let { extension } = item; if (extension.isActive) return; extension.activate().then(() => { if (extension.isActive) { this._onDidActiveExtension.fire(extension); } }, e => { workspace_1.default.showMessage(`Error on activate ${extension.id}: ${e.message}`, 'error'); logger.error(`Error on activate extension ${extension.id}:`, e); }); } deactivate(id) { let item = this.list.find(o => o.id == id); if (!item) return false; if (item.extension.isActive && typeof item.deactivate == 'function') { item.deactivate(); return true; } return false; } async call(id, method, args) { let item = this.list.find(o => o.id == id); if (!item) return workspace_1.default.showMessage(`extension ${id} not found`, 'error'); let { extension } = item; if (!extension.isActive) { workspace_1.default.showMessage(`extension ${id} not activated`, 'error'); return; } let { exports } = extension; if (!exports || !exports.hasOwnProperty(method)) { workspace_1.default.showMessage(`method ${method} not found on extension ${id}`, 'error'); return; } return await Promise.resolve(exports[method].apply(null, args)); } getExtensionApi(id) { let item = this.list.find(o => o.id == id); if (!item) return null; let { extension } = item; return extension.isActive ? extension.exports : null; } registerExtension(extension, deactivate) { let { id, packageJSON } = extension; this.list.push({ id, extension, deactivate, isLocal: true }); let { contributes } = packageJSON; if (contributes) { let { configuration } = contributes; if (configuration && configuration.properties) { let { properties } = configuration; let props = {}; for (let key of Object.keys(properties)) { let val = properties[key].default; if (val != null) props[key] = val; } workspace_1.default.configurations.extendsDefaults(props); } } this._onDidLoadExtension.fire(extension); this.setupActiveEvents(id, packageJSON); } get globalExtensions() { let json = this.loadJson(); if (!json || !json.dependencies) return []; return Object.keys(json.dependencies); } async globalExtensionStats() { let json = this.loadJson(); if (!json || !json.dependencies) return []; let res = await Promise.all(Object.keys(json.dependencies).map(key => { return new Promise(async (resolve) => { try { let val = json.dependencies[key]; let root = path_1.default.join(this.root, 'node_modules', key); let jsonFile = path_1.default.join(root, 'package.json'); let stat = await fs_2.statAsync(jsonFile); if (!stat || !stat.isFile()) return resolve(null); let content = await fs_2.readFile(jsonFile, 'utf8'); root = await fs_2.realpathAsync(root); let obj = JSON.parse(content); let { engines } = obj; if (!engines || (!engines.hasOwnProperty('coc') && !engines.hasOwnProperty('vscode'))) { return resolve(null); } let version = obj ? obj.version || '' : ''; let description = obj ? obj.description || '' : ''; let uri = isuri_1.default.isValid(val) ? val : null; resolve({ id: key, isLocal: false, version, description, exotic: /^https?:/.test(val), uri, root, state: this.getExtensionState(key) }); } catch (e) { logger.error(e); resolve(null); } }); })); return res.filter(info => info != null); } async localExtensionStats(exclude) { let runtimepath = await workspace_1.default.nvim.eval('&runtimepath'); let included = exclude.map(o => o.root); let names = exclude.map(o => o.id); let paths = runtimepath.split(','); let res = await Promise.all(paths.map(root => { return new Promise(async (resolve) => { try { if (included.includes(root)) { return resolve(null); } let jsonFile = path_1.default.join(root, 'package.json'); let stat = await fs_2.statAsync(jsonFile); if (!stat || !stat.isFile()) return resolve(null); let content = await fs_2.readFile(jsonFile, 'utf8'); let obj = JSON.parse(content); let { engines } = obj; if (!engines || (!engines.hasOwnProperty('coc') && !engines.hasOwnProperty('vscode'))) { return resolve(null); } if (names.indexOf(obj.name) !== -1) { workspace_1.default.showMessage(`Skipped extension "${root}", please uninstall "${obj.name}" by :CocUninstall ${obj.name}`, 'warning'); return resolve(null); } let version = obj ? obj.version || '' : ''; let description = obj ? obj.description || '' : ''; resolve({ id: obj.name, isLocal: true, version, description, exotic: false, root, state: this.getExtensionState(obj.name) }); } catch (e) { logger.error(e); resolve(null); } }); })); return res.filter(info => info != null); } isGlobalExtension(id) { return this.globalExtensions.indexOf(id) !== -1; } loadJson() { let { root } = this; let jsonFile = path_1.default.join(root, 'package.json'); if (!fs_1.default.existsSync(jsonFile)) return null; return loadJson(jsonFile); } get schemes() { return this._additionalSchemes; } addSchemeProperty(key, def) { this._additionalSchemes[key] = def; workspace_1.default.configurations.extendsDefaults({ [key]: def.default }); } setupActiveEvents(id, packageJSON) { let { activationEvents } = packageJSON; if (!activationEvents || activationEvents.indexOf('*') !== -1 || !Array.isArray(activationEvents)) { this.activate(id); return; } let active = () => { util_2.disposeAll(disposables); this.activate(id); active = () => { }; // tslint:disable-line }; let disposables = []; for (let eventName of activationEvents) { let parts = eventName.split(':'); let ev = parts[0]; if (ev == 'onLanguage') { if (workspace_1.default.filetypes.has(parts[1])) { active(); return; } workspace_1.default.onDidOpenTextDocument(document => { if (document.languageId == parts[1]) { active(); } }, null, disposables); } else if (ev == 'onCommand') { events_1.default.on('Command', command => { if (command == parts[1]) { active(); // wait for service ready return new Promise(resolve => { setTimeout(resolve, 500); }); } }, null, disposables); } else if (ev == 'workspaceContains') { let check = () => { let folders = workspace_1.default.workspaceFolders.map(o => vscode_uri_1.URI.parse(o.uri).fsPath); for (let folder of folders) { if (fs_2.inDirectory(folder, parts[1].split(/\s+/))) { active(); break; } } }; check(); workspace_1.default.onDidChangeWorkspaceFolders(check, null, disposables); } else if (ev == 'onFileSystem') { for (let doc of workspace_1.default.documents) { let u = vscode_uri_1.URI.parse(doc.uri); if (u.scheme == parts[1]) { return active(); } } workspace_1.default.onDidOpenTextDocument(document => { let u = vscode_uri_1.URI.parse(document.uri); if (u.scheme == parts[1]) { active(); } }, null, disposables); } else { workspace_1.default.showMessage(`Unsupported event ${eventName} of ${id}`, 'error'); } } } createExtension(root, packageJSON, isLocal = false) { let id = `${packageJSON.name}`; let isActive = false; let exports = null; let filename = path_1.default.join(root, packageJSON.main || 'index.js'); let ext; let subscriptions = []; let extension = { activate: async () => { if (isActive) return; let context = { subscriptions, extensionPath: root, globalState: this.memos.createMemento(`${id}|global`), workspaceState: this.memos.createMemento(`${id}|${workspace_1.default.rootPath}`), asAbsolutePath: relativePath => { return path_1.default.join(root, relativePath); }, storagePath: path_1.default.join(this.root, `${id}-data`), logger: createLogger(id) }; isActive = true; if (!ext) { try { ext = factory_1.createExtension(id, filename); } catch (e) { workspace_1.default.showMessage(`Error on load extension ${id} from ${filename}: ${e}`, 'error'); logger.error(e); return; } } try { exports = await Promise.resolve(ext.activate(context)); } catch (e) { isActive = false; workspace_1.default.showMessage(`Error on active extension ${id}: ${e}`, 'error'); logger.error(e); } return exports; } }; Object.defineProperties(extension, { id: { get: () => id }, packageJSON: { get: () => packageJSON }, extensionPath: { get: () => root }, isActive: { get: () => isActive }, exports: { get: () => exports } }); this.list.push({ id, isLocal, extension, directory: root, deactivate: () => { isActive = false; if (ext && ext.deactivate) { Promise.resolve(ext.deactivate()).catch(e => { logger.error(`Error on ${id} deactivate: `, e.message); }); } util_2.disposeAll(subscriptions); subscriptions = []; } }); let { contributes } = packageJSON; if (contributes) { let { configuration, rootPatterns, commands } = contributes; if (configuration && configuration.properties) { let { properties } = configuration; let props = {}; for (let key of Object.keys(properties)) { let val = properties[key].default; if (val != null) props[key] = val; } workspace_1.default.configurations.extendsDefaults(props); } if (rootPatterns && rootPatterns.length) { for (let item of rootPatterns) { workspace_1.default.addRootPatterns(item.filetype, item.patterns); } } if (commands && commands.length) { for (let cmd of commands) { commands_1.default.titles.set(cmd.command, cmd.title); } } } this._onDidLoadExtension.fire(extension); if (this.activated) { this.setupActiveEvents(id, packageJSON); } return id; } async initializeRoot() { let root = this.root = await workspace_1.default.nvim.call('coc#util#extension_root'); if (!fs_1.default.existsSync(root)) { mkdirp_1.default.sync(root); } let jsonFile = path_1.default.join(root, 'package.json'); if (!fs_1.default.existsSync(jsonFile)) { fs_1.default.writeFileSync(jsonFile, '{"dependencies":{}}', 'utf8'); } if (!this.db) { let filepath = path_1.default.join(root, 'db.json'); this.db = new db_1.default(filepath); } this.manager = new extension_1.default(root); } } exports.Extensions = Extensions; exports.default = new Extensions(); //# sourceMappingURL=extensions.js.map