UNPKG

@chemzqm/neovim

Version:

NodeJS client API for vim9 and neovim

376 lines (375 loc) 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NeovimClient = exports.AsyncResponse = void 0; /** * Handles attaching transport */ const events_1 = require("events"); const nvim_1 = require("../transport/nvim"); const vim_1 = require("../transport/vim"); const constants_1 = require("../utils/constants"); const Buffer_1 = require("./Buffer"); const Neovim_1 = require("./Neovim"); const Tabpage_1 = require("./Tabpage"); const Window_1 = require("./Window"); const functionsOnVim = [ 'nvim_buf_attach', 'nvim_get_mode', 'nvim_list_runtime_paths', 'nvim_win_del_var', 'nvim_create_buf', 'nvim_exec', 'nvim_tabpage_list_wins', 'nvim_buf_del_var', 'nvim_buf_get_mark', 'nvim_tabpage_set_var', 'nvim_create_namespace', 'nvim_win_get_position', 'nvim_win_set_height', 'nvim_call_atomic', 'nvim_buf_detach', 'nvim_buf_line_count', 'nvim_set_current_buf', 'nvim_set_current_dir', 'nvim_get_var', 'nvim_del_current_line', 'nvim_win_set_width', 'nvim_out_write', 'nvim_win_is_valid', 'nvim_set_current_win', 'nvim_get_current_tabpage', 'nvim_tabpage_is_valid', 'nvim_set_var', 'nvim_win_get_height', 'nvim_win_get_buf', 'nvim_win_get_width', 'nvim_buf_set_name', 'nvim_subscribe', 'nvim_get_current_win', 'nvim_feedkeys', 'nvim_get_vvar', 'nvim_tabpage_get_number', 'nvim_get_current_buf', 'nvim_win_get_option', 'nvim_win_get_cursor', 'nvim_get_current_line', 'nvim_win_get_var', 'nvim_buf_get_var', 'nvim_set_current_tabpage', 'nvim_buf_clear_namespace', 'nvim_err_write', 'nvim_del_var', 'nvim_call_dict_function', 'nvim_set_current_line', 'nvim_get_api_info', 'nvim_unsubscribe', 'nvim_get_option', 'nvim_list_wins', 'nvim_set_client_info', 'nvim_win_set_cursor', 'nvim_win_set_option', 'nvim_eval', 'nvim_tabpage_get_var', 'nvim_buf_get_option', 'nvim_tabpage_del_var', 'nvim_buf_get_name', 'nvim_list_bufs', 'nvim_win_set_buf', 'nvim_win_close', 'nvim_command_output', 'nvim_command', 'nvim_tabpage_get_win', 'nvim_win_set_var', 'nvim_buf_add_highlight', 'nvim_buf_set_var', 'nvim_win_get_number', 'nvim_strwidth', 'nvim_buf_set_lines', 'nvim_err_writeln', 'nvim_buf_set_option', 'nvim_list_tabpages', 'nvim_set_option', 'nvim_buf_get_lines', 'nvim_buf_get_changedtick', 'nvim_win_get_tabpage', 'nvim_call_function', 'nvim_buf_is_valid' ]; class AsyncResponse { constructor(requestId, cb) { this.requestId = requestId; this.cb = cb; this.finished = false; } finish(err, res) { if (this.finished) return; this.finished = true; if (err) { this.cb(new Error(err)); return; } this.cb(null, res); } } exports.AsyncResponse = AsyncResponse; function applyMixins(derivedCtor, constructors) { constructors.forEach((baseCtor) => { Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null)); }); }); } class NeovimClient extends Neovim_1.Neovim { constructor(logger, isVim) { // Neovim has no `data` or `metadata` super({}); this.logger = logger; this.isVim = isVim; this.requestId = 1; this.responses = new Map(); this.attachedBuffers = new Map(); const transport = isVim ? new vim_1.VimTransport(logger) : new nvim_1.NvimTransport(logger); Object.defineProperty(this, '_transport', { enumerable: false, get: () => { return transport; } }); this.handleRequest = this.handleRequest.bind(this); this.handleNotification = this.handleNotification.bind(this); } get transport() { return this._transport; } echoError(msg) { let prefix = constants_1.isCocNvim ? '[coc.nvim] ' : ''; if (msg instanceof Error) { if (!constants_1.isTester) this.errWriteLine(prefix + msg.message + ' use :CocOpenLog for details'); this.logError(msg.message || 'Unknown error', msg); } else { if (!constants_1.isTester) this.errWriteLine(prefix + msg); this.logError(msg.toString(), new Error()); } } logError(msg, ...args) { if (constants_1.isTester) console.error(msg, ...args); if (!this.logger) return; this.logger.error(msg, ...args); } createBuffer(id) { return new Buffer_1.Buffer({ data: id, client: this }); } createWindow(id) { return new Window_1.Window({ data: id, client: this }); } createTabpage(id) { return new Tabpage_1.Tabpage({ data: id, client: this }); } /** * Invoke redraw on vim, must called when screen need update. */ redrawVim(force) { if (!this.isVim) return; // Don't use this, can cause cursor vanish. // this.transport.vimCommand('redraw', force) this.transport.notify('nvim_command', [`redraw${force ? '!' : ''}`]); } /** Attaches msgpack to read/write streams * */ attach({ reader, writer, }, requestApi = true) { this.transport.attach(writer, reader, this); this.transport.on('request', this.handleRequest); this.transport.on('notification', this.handleNotification); this.transport.on('detach', () => { this.emit('disconnect'); this.transport.removeAllListeners('request'); this.transport.removeAllListeners('notification'); this.transport.removeAllListeners('detach'); }); if (requestApi) { this._isReady = this.generateApi().catch(err => { this.logger.error(err); return false; }); } else { this._channelId = -1; this._isReady = Promise.resolve(true); } } /* called when attach process disconnected*/ detach() { this.attachedBuffers.clear(); this.transport.detach(); this.removeAllListeners(); } get channelId() { return this._isReady.then(() => { return this._channelId; }); } handleRequest(method, args, resp) { this.emit('request', method, args, resp); } sendAsyncRequest(method, args) { let id = this.requestId; this.requestId = id + 1; this.notify('nvim_call_function', ['coc#rpc#async_request', [id, method, args || []]]); return new Promise((resolve, reject) => { let response = new AsyncResponse(id, (err, res) => { if (err) return reject(err); resolve(res); }); this.responses.set(id, response); }); } handleNotification(method, args) { if (method.endsWith('_event')) { if (method == 'vim_buf_change_event') { const id = args[0]; if (!this.attachedBuffers.has(id)) return; const bufferMap = this.attachedBuffers.get(id); const cbs = bufferMap.get('vim_lines') || []; cbs.forEach(cb => cb(...args)); return; } if (method.startsWith('nvim_buf_')) { const shortName = method.replace(/nvim_buf_(.*)_event/, '$1'); const { id } = args[0]; if (!this.attachedBuffers.has(id)) return; const bufferMap = this.attachedBuffers.get(id); const cbs = bufferMap.get(shortName) || []; cbs.forEach(cb => cb(...args)); // Handle `nvim_buf_detach_event` // clean `attachedBuffers` since it will no longer be attached if (shortName === 'detach') { this.attachedBuffers.delete(id); } return; } // async_request_event from vim if (method == 'nvim_async_request_event') { const [id, method, arr] = args; this.handleRequest(method, arr, { send: (resp, isError) => { this.notify('nvim_call_function', ['coc#rpc#async_response', [id, resp, isError]]); } }); return; } // nvim_async_response_event if (method == 'nvim_async_response_event') { const [id, err, res] = args; const response = this.responses.get(id); if (!response) { this.logError(`Response not found for request ${id}`); return; } this.responses.delete(id); response.finish(err, res); return; } if (method === 'nvim_error_event') { this.logger.error(`Error event from nvim:`, args[0], args[1]); this.emit('vim_error', args[1]); return; } this.logger.warn(`Unhandled event: ${method}`, args); } else { this.emit('notification', method, args); } } requestApi() { return new Promise((resolve, reject) => { this.transport.request('nvim_get_api_info', [], (err, res) => { if (err) { reject(new Error(Array.isArray(err) ? err[1] : err.message || err.toString())); } else { resolve(res); } }); }); } async generateApi() { let results = await this.requestApi(); const [channelId, metadata] = results; // TODO metadata not used // this.functions = metadata.functions.map(f => f.name) this._channelId = channelId; return true; } attachBufferEvent(bufnr, eventName, cb) { const bufferMap = this.attachedBuffers.get(bufnr) || new Map(); const cbs = bufferMap.get(eventName) || []; if (cbs.includes(cb)) return; cbs.push(cb); bufferMap.set(eventName, cbs); this.attachedBuffers.set(bufnr, bufferMap); return; } /** * Returns `true` if buffer should be detached */ detachBufferEvent(bufnr, eventName, cb) { const bufferMap = this.attachedBuffers.get(bufnr); if (!bufferMap || !bufferMap.has(eventName)) return; const handlers = bufferMap.get(eventName).filter(handler => handler !== cb); bufferMap.set(eventName, handlers); } pauseNotification() { let o = {}; Error.captureStackTrace(o); if (this.transport.pauseLevel != 0) { this.logError(`Nested nvim.pauseNotification() detected, please avoid it:`, o.stack); } this.transport.pauseNotification(); process.nextTick(() => { if (this.transport.pauseLevel > 0) { this.logError(`resumeNotification not called within same tick:`, o.stack); } }); } resumeNotification(redrawVim, notify) { if (this.isVim && redrawVim) { this.transport.notify('nvim_command', ['redraw']); } if (notify) { this.transport.resumeNotification(true); return Promise.resolve(null); } return this.transport.resumeNotification(); } /** * @deprecated */ hasFunction(name) { if (!this.isVim) return true; return functionsOnVim.includes(name); } } exports.NeovimClient = NeovimClient; applyMixins(NeovimClient, [events_1.EventEmitter]);