@chemzqm/neovim
Version:
NodeJS client API for vim9 and neovim
376 lines (375 loc) • 12.2 kB
JavaScript
"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]);