--- /dev/null
+"""Main Nvim interface."""
+import os
+import sys
+import threading
+from functools import partial
+from traceback import format_stack
+
+from msgpack import ExtType
+
+from pynvim.api.buffer import Buffer
+from pynvim.api.common import (NvimError, Remote, RemoteApi, RemoteMap, RemoteSequence,
+ decode_if_bytes, walk)
+from pynvim.api.tabpage import Tabpage
+from pynvim.api.window import Window
+from pynvim.compat import IS_PYTHON3
+from pynvim.util import Version, format_exc_skip
+
+__all__ = ('Nvim')
+
+
+os_chdir = os.chdir
+
+lua_module = """
+local a = vim.api
+local function update_highlights(buf, src_id, hls, clear_first, clear_end)
+ if clear_first ~= nil then
+ a.nvim_buf_clear_highlight(buf, src_id, clear_first, clear_end)
+ end
+ for _,hl in pairs(hls) do
+ local group, line, col_start, col_end = unpack(hl)
+ if col_start == nil then
+ col_start = 0
+ end
+ if col_end == nil then
+ col_end = -1
+ end
+ a.nvim_buf_add_highlight(buf, src_id, group, line, col_start, col_end)
+ end
+end
+
+local chid = ...
+local mod = {update_highlights=update_highlights}
+_G["_pynvim_"..chid] = mod
+"""
+
+
+class Nvim(object):
+
+ """Class that represents a remote Nvim instance.
+
+ This class is main entry point to Nvim remote API, it is a wrapper
+ around Session instances.
+
+ The constructor of this class must not be called directly. Instead, the
+ `from_session` class method should be used to create the first instance
+ from a raw `Session` instance.
+
+ Subsequent instances for the same session can be created by calling the
+ `with_decode` instance method to change the decoding behavior or
+ `SubClass.from_nvim(nvim)` where `SubClass` is a subclass of `Nvim`, which
+ is useful for having multiple `Nvim` objects that behave differently
+ without one affecting the other.
+
+ When this library is used on python3.4+, asyncio event loop is guaranteed
+ to be used. It is available as the "loop" attribute of this class. Note
+ that asyncio callbacks cannot make blocking requests, which includes
+ accessing state-dependent attributes. They should instead schedule another
+ callback using nvim.async_call, which will not have this restriction.
+ """
+
+ @classmethod
+ def from_session(cls, session):
+ """Create a new Nvim instance for a Session instance.
+
+ This method must be called to create the first Nvim instance, since it
+ queries Nvim metadata for type information and sets a SessionHook for
+ creating specialized objects from Nvim remote handles.
+ """
+ session.error_wrapper = lambda e: NvimError(decode_if_bytes(e[1]))
+ channel_id, metadata = session.request(b'nvim_get_api_info')
+
+ if IS_PYTHON3:
+ # decode all metadata strings for python3
+ metadata = walk(decode_if_bytes, metadata)
+
+ types = {
+ metadata['types']['Buffer']['id']: Buffer,
+ metadata['types']['Window']['id']: Window,
+ metadata['types']['Tabpage']['id']: Tabpage,
+ }
+
+ return cls(session, channel_id, metadata, types)
+
+ @classmethod
+ def from_nvim(cls, nvim):
+ """Create a new Nvim instance from an existing instance."""
+ return cls(nvim._session, nvim.channel_id, nvim.metadata,
+ nvim.types, nvim._decode, nvim._err_cb)
+
+ def __init__(self, session, channel_id, metadata, types,
+ decode=False, err_cb=None):
+ """Initialize a new Nvim instance. This method is module-private."""
+ self._session = session
+ self.channel_id = channel_id
+ self.metadata = metadata
+ version = metadata.get("version", {"api_level": 0})
+ self.version = Version(**version)
+ self.types = types
+ self.api = RemoteApi(self, 'nvim_')
+ self.vars = RemoteMap(self, 'nvim_get_var', 'nvim_set_var', 'nvim_del_var')
+ self.vvars = RemoteMap(self, 'nvim_get_vvar', None, None)
+ self.options = RemoteMap(self, 'nvim_get_option', 'nvim_set_option')
+ self.buffers = Buffers(self)
+ self.windows = RemoteSequence(self, 'nvim_list_wins')
+ self.tabpages = RemoteSequence(self, 'nvim_list_tabpages')
+ self.current = Current(self)
+ self.session = CompatibilitySession(self)
+ self.funcs = Funcs(self)
+ self.lua = LuaFuncs(self)
+ self.error = NvimError
+ self._decode = decode
+ self._err_cb = err_cb
+
+ # only on python3.4+ we expose asyncio
+ if IS_PYTHON3:
+ self.loop = self._session.loop._loop
+
+ def _from_nvim(self, obj, decode=None):
+ if decode is None:
+ decode = self._decode
+ if type(obj) is ExtType:
+ cls = self.types[obj.code]
+ return cls(self, (obj.code, obj.data))
+ if decode:
+ obj = decode_if_bytes(obj, decode)
+ return obj
+
+ def _to_nvim(self, obj):
+ if isinstance(obj, Remote):
+ return ExtType(*obj.code_data)
+ return obj
+
+ def _get_lua_private(self):
+ if not getattr(self._session, "_has_lua", False):
+ self.exec_lua(lua_module, self.channel_id)
+ self._session._has_lua = True
+ return getattr(self.lua, "_pynvim_{}".format(self.channel_id))
+
+ def request(self, name, *args, **kwargs):
+ r"""Send an API request or notification to nvim.
+
+ It is rarely needed to call this function directly, as most API
+ functions have python wrapper functions. The `api` object can
+ be also be used to call API functions as methods:
+
+ vim.api.err_write('ERROR\n', async_=True)
+ vim.current.buffer.api.get_mark('.')
+
+ is equivalent to
+
+ vim.request('nvim_err_write', 'ERROR\n', async_=True)
+ vim.request('nvim_buf_get_mark', vim.current.buffer, '.')
+
+
+ Normally a blocking request will be sent. If the `async_` flag is
+ present and True, a asynchronous notification is sent instead. This
+ will never block, and the return value or error is ignored.
+ """
+ if (self._session._loop_thread is not None
+ and threading.current_thread() != self._session._loop_thread):
+
+ msg = ("Request from non-main thread.\n"
+ "Requests from different threads should be wrapped "
+ "with nvim.async_call(cb, ...) \n{}\n"
+ .format('\n'.join(format_stack(None, 5)[:-1])))
+
+ self.async_call(self._err_cb, msg)
+ raise NvimError("request from non-main thread")
+
+ decode = kwargs.pop('decode', self._decode)
+ args = walk(self._to_nvim, args)
+ res = self._session.request(name, *args, **kwargs)
+ return walk(self._from_nvim, res, decode=decode)
+
+ def next_message(self):
+ """Block until a message(request or notification) is available.
+
+ If any messages were previously enqueued, return the first in queue.
+ If not, run the event loop until one is received.
+ """
+ msg = self._session.next_message()
+ if msg:
+ return walk(self._from_nvim, msg)
+
+ def run_loop(self, request_cb, notification_cb,
+ setup_cb=None, err_cb=None):
+ """Run the event loop to receive requests and notifications from Nvim.
+
+ This should not be called from a plugin running in the host, which
+ already runs the loop and dispatches events to plugins.
+ """
+ if err_cb is None:
+ err_cb = sys.stderr.write
+ self._err_cb = err_cb
+
+ def filter_request_cb(name, args):
+ name = self._from_nvim(name)
+ args = walk(self._from_nvim, args)
+ try:
+ result = request_cb(name, args)
+ except Exception:
+ msg = ("error caught in request handler '{} {}'\n{}\n\n"
+ .format(name, args, format_exc_skip(1)))
+ self._err_cb(msg)
+ raise
+ return walk(self._to_nvim, result)
+
+ def filter_notification_cb(name, args):
+ name = self._from_nvim(name)
+ args = walk(self._from_nvim, args)
+ try:
+ notification_cb(name, args)
+ except Exception:
+ msg = ("error caught in notification handler '{} {}'\n{}\n\n"
+ .format(name, args, format_exc_skip(1)))
+ self._err_cb(msg)
+ raise
+
+ self._session.run(filter_request_cb, filter_notification_cb, setup_cb)
+
+ def stop_loop(self):
+ """Stop the event loop being started with `run_loop`."""
+ self._session.stop()
+
+ def close(self):
+ """Close the nvim session and release its resources."""
+ self._session.close()
+
+ def __enter__(self):
+ """Enter nvim session as a context manager."""
+ return self
+
+ def __exit__(self, *exc_info):
+ """Exit nvim session as a context manager.
+
+ Closes the event loop.
+ """
+ self.close()
+
+ def with_decode(self, decode=True):
+ """Initialize a new Nvim instance."""
+ return Nvim(self._session, self.channel_id,
+ self.metadata, self.types, decode, self._err_cb)
+
+ def ui_attach(self, width, height, rgb=None, **kwargs):
+ """Register as a remote UI.
+
+ After this method is called, the client will receive redraw
+ notifications.
+ """
+ options = kwargs
+ if rgb is not None:
+ options['rgb'] = rgb
+ return self.request('nvim_ui_attach', width, height, options)
+
+ def ui_detach(self):
+ """Unregister as a remote UI."""
+ return self.request('nvim_ui_detach')
+
+ def ui_try_resize(self, width, height):
+ """Notify nvim that the client window has resized.
+
+ If possible, nvim will send a redraw request to resize.
+ """
+ return self.request('ui_try_resize', width, height)
+
+ def subscribe(self, event):
+ """Subscribe to a Nvim event."""
+ return self.request('nvim_subscribe', event)
+
+ def unsubscribe(self, event):
+ """Unsubscribe to a Nvim event."""
+ return self.request('nvim_unsubscribe', event)
+
+ def command(self, string, **kwargs):
+ """Execute a single ex command."""
+ return self.request('nvim_command', string, **kwargs)
+
+ def command_output(self, string):
+ """Execute a single ex command and return the output."""
+ return self.request('nvim_command_output', string)
+
+ def eval(self, string, **kwargs):
+ """Evaluate a vimscript expression."""
+ return self.request('nvim_eval', string, **kwargs)
+
+ def call(self, name, *args, **kwargs):
+ """Call a vimscript function."""
+ return self.request('nvim_call_function', name, args, **kwargs)
+
+ def exec_lua(self, code, *args, **kwargs):
+ """Execute lua code.
+
+ Additional parameters are available as `...` inside the lua chunk.
+ Only statements are executed. To evaluate an expression, prefix it
+ with `return`: `return my_function(...)`
+
+ There is a shorthand syntax to call lua functions with arguments:
+
+ nvim.lua.func(1,2)
+ nvim.lua.mymod.myfunction(data, async_=True)
+
+ is equivalent to
+
+ nvim.exec_lua("return func(...)", 1, 2)
+ nvim.exec_lua("mymod.myfunction(...)", data, async_=True)
+
+ Note that with `async_=True` there is no return value.
+ """
+ return self.request('nvim_execute_lua', code, args, **kwargs)
+
+ def strwidth(self, string):
+ """Return the number of display cells `string` occupies.
+
+ Tab is counted as one cell.
+ """
+ return self.request('nvim_strwidth', string)
+
+ def list_runtime_paths(self):
+ """Return a list of paths contained in the 'runtimepath' option."""
+ return self.request('nvim_list_runtime_paths')
+
+ def foreach_rtp(self, cb):
+ """Invoke `cb` for each path in 'runtimepath'.
+
+ Call the given callable for each path in 'runtimepath' until either
+ callable returns something but None, the exception is raised or there
+ are no longer paths. If stopped in case callable returned non-None,
+ vim.foreach_rtp function returns the value returned by callable.
+ """
+ for path in self.request('nvim_list_runtime_paths'):
+ try:
+ if cb(path) is not None:
+ break
+ except Exception:
+ break
+
+ def chdir(self, dir_path):
+ """Run os.chdir, then all appropriate vim stuff."""
+ os_chdir(dir_path)
+ return self.request('nvim_set_current_dir', dir_path)
+
+ def feedkeys(self, keys, options='', escape_csi=True):
+ """Push `keys` to Nvim user input buffer.
+
+ Options can be a string with the following character flags:
+ - 'm': Remap keys. This is default.
+ - 'n': Do not remap keys.
+ - 't': Handle keys as if typed; otherwise they are handled as if coming
+ from a mapping. This matters for undo, opening folds, etc.
+ """
+ return self.request('nvim_feedkeys', keys, options, escape_csi)
+
+ def input(self, bytes):
+ """Push `bytes` to Nvim low level input buffer.
+
+ Unlike `feedkeys()`, this uses the lowest level input buffer and the
+ call is not deferred. It returns the number of bytes actually
+ written(which can be less than what was requested if the buffer is
+ full).
+ """
+ return self.request('nvim_input', bytes)
+
+ def replace_termcodes(self, string, from_part=False, do_lt=True,
+ special=True):
+ r"""Replace any terminal code strings by byte sequences.
+
+ The returned sequences are Nvim's internal representation of keys,
+ for example:
+
+ <esc> -> '\x1b'
+ <cr> -> '\r'
+ <c-l> -> '\x0c'
+ <up> -> '\x80ku'
+
+ The returned sequences can be used as input to `feedkeys`.
+ """
+ return self.request('nvim_replace_termcodes', string,
+ from_part, do_lt, special)
+
+ def out_write(self, msg, **kwargs):
+ r"""Print `msg` as a normal message.
+
+ The message is buffered (won't display) until linefeed ("\n").
+ """
+ return self.request('nvim_out_write', msg, **kwargs)
+
+ def err_write(self, msg, **kwargs):
+ r"""Print `msg` as an error message.
+
+ The message is buffered (won't display) until linefeed ("\n").
+ """
+ if self._thread_invalid():
+ # special case: if a non-main thread writes to stderr
+ # i.e. due to an uncaught exception, pass it through
+ # without raising an additional exception.
+ self.async_call(self.err_write, msg, **kwargs)
+ return
+ return self.request('nvim_err_write', msg, **kwargs)
+
+ def _thread_invalid(self):
+ return (self._session._loop_thread is not None
+ and threading.current_thread() != self._session._loop_thread)
+
+ def quit(self, quit_command='qa!'):
+ """Send a quit command to Nvim.
+
+ By default, the quit command is 'qa!' which will make Nvim quit without
+ saving anything.
+ """
+ try:
+ self.command(quit_command)
+ except OSError:
+ # sending a quit command will raise an IOError because the
+ # connection is closed before a response is received. Safe to
+ # ignore it.
+ pass
+
+ def new_highlight_source(self):
+ """Return new src_id for use with Buffer.add_highlight."""
+ return self.current.buffer.add_highlight("", 0, src_id=0)
+
+ def async_call(self, fn, *args, **kwargs):
+ """Schedule `fn` to be called by the event loop soon.
+
+ This function is thread-safe, and is the only way code not
+ on the main thread could interact with nvim api objects.
+
+ This function can also be called in a synchronous
+ event handler, just before it returns, to defer execution
+ that shouldn't block neovim.
+ """
+ call_point = ''.join(format_stack(None, 5)[:-1])
+
+ def handler():
+ try:
+ fn(*args, **kwargs)
+ except Exception as err:
+ msg = ("error caught while executing async callback:\n"
+ "{!r}\n{}\n \nthe call was requested at\n{}"
+ .format(err, format_exc_skip(1), call_point))
+ self._err_cb(msg)
+ raise
+ self._session.threadsafe_call(handler)
+
+
+class Buffers(object):
+
+ """Remote NVim buffers.
+
+ Currently the interface for interacting with remote NVim buffers is the
+ `nvim_list_bufs` msgpack-rpc function. Most methods fetch the list of
+ buffers from NVim.
+
+ Conforms to *python-buffers*.
+ """
+
+ def __init__(self, nvim):
+ """Initialize a Buffers object with Nvim object `nvim`."""
+ self._fetch_buffers = nvim.api.list_bufs
+
+ def __len__(self):
+ """Return the count of buffers."""
+ return len(self._fetch_buffers())
+
+ def __getitem__(self, number):
+ """Return the Buffer object matching buffer number `number`."""
+ for b in self._fetch_buffers():
+ if b.number == number:
+ return b
+ raise KeyError(number)
+
+ def __contains__(self, b):
+ """Return whether Buffer `b` is a known valid buffer."""
+ return isinstance(b, Buffer) and b.valid
+
+ def __iter__(self):
+ """Return an iterator over the list of buffers."""
+ return iter(self._fetch_buffers())
+
+
+class CompatibilitySession(object):
+
+ """Helper class for API compatibility."""
+
+ def __init__(self, nvim):
+ self.threadsafe_call = nvim.async_call
+
+
+class Current(object):
+
+ """Helper class for emulating vim.current from python-vim."""
+
+ def __init__(self, session):
+ self._session = session
+ self.range = None
+
+ @property
+ def line(self):
+ return self._session.request('nvim_get_current_line')
+
+ @line.setter
+ def line(self, line):
+ return self._session.request('nvim_set_current_line', line)
+
+ @line.deleter
+ def line(self):
+ return self._session.request('nvim_del_current_line')
+
+ @property
+ def buffer(self):
+ return self._session.request('nvim_get_current_buf')
+
+ @buffer.setter
+ def buffer(self, buffer):
+ return self._session.request('nvim_set_current_buf', buffer)
+
+ @property
+ def window(self):
+ return self._session.request('nvim_get_current_win')
+
+ @window.setter
+ def window(self, window):
+ return self._session.request('nvim_set_current_win', window)
+
+ @property
+ def tabpage(self):
+ return self._session.request('nvim_get_current_tabpage')
+
+ @tabpage.setter
+ def tabpage(self, tabpage):
+ return self._session.request('nvim_set_current_tabpage', tabpage)
+
+
+class Funcs(object):
+
+ """Helper class for functional vimscript interface."""
+
+ def __init__(self, nvim):
+ self._nvim = nvim
+
+ def __getattr__(self, name):
+ return partial(self._nvim.call, name)
+
+
+class LuaFuncs(object):
+
+ """Wrapper to allow lua functions to be called like python methods."""
+
+ def __init__(self, nvim, name=""):
+ self._nvim = nvim
+ self.name = name
+
+ def __getattr__(self, name):
+ """Return wrapper to named api method."""
+ prefix = self.name + "." if self.name else ""
+ return LuaFuncs(self._nvim, prefix + name)
+
+ def __call__(self, *args, **kwargs):
+ # first new function after keyword rename, be a bit noisy
+ if 'async' in kwargs:
+ raise ValueError('"async" argument is not allowed. '
+ 'Use "async_" instead.')
+ async_ = kwargs.get('async_', False)
+ pattern = "return {}(...)" if not async_ else "{}(...)"
+ code = pattern.format(self.name)
+ return self._nvim.exec_lua(code, *args, **kwargs)