neovim-0.2.0/0000755000175000017500000000000013200646543013257 5ustar bjornbjorn00000000000000neovim-0.2.0/neovim/0000755000175000017500000000000013200646543014554 5ustar bjornbjorn00000000000000neovim-0.2.0/neovim/msgpack_rpc/0000755000175000017500000000000013200646543017045 5ustar bjornbjorn00000000000000neovim-0.2.0/neovim/msgpack_rpc/session.py0000644000175000017500000001770313200645644021113 0ustar bjornbjorn00000000000000"""Synchronous msgpack-rpc session layer.""" import logging from collections import deque from traceback import format_exc import greenlet logger = logging.getLogger(__name__) error, debug, info, warn = (logger.error, logger.debug, logger.info, logger.warning,) class Session(object): """Msgpack-rpc session layer that uses coroutines for a synchronous API. This class provides the public msgpack-rpc API required by this library. It uses the greenlet module to handle requests and notifications coming from Nvim with a synchronous API. """ def __init__(self, async_session): """Wrap `async_session` on a synchronous msgpack-rpc interface.""" self._async_session = async_session self._request_cb = self._notification_cb = None self._pending_messages = deque() self._is_running = False self._setup_exception = None def threadsafe_call(self, fn, *args, **kwargs): """Wrapper around `AsyncSession.threadsafe_call`.""" def handler(): try: fn(*args, **kwargs) except Exception: warn("error caught while excecuting async callback\n%s\n", format_exc()) def greenlet_wrapper(): gr = greenlet.greenlet(handler) gr.switch() self._async_session.threadsafe_call(greenlet_wrapper) 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. """ if self._is_running: raise Exception('Event loop already running') if self._pending_messages: return self._pending_messages.popleft() self._async_session.run(self._enqueue_request_and_stop, self._enqueue_notification_and_stop) if self._pending_messages: return self._pending_messages.popleft() def request(self, method, *args, **kwargs): """Send a msgpack-rpc request and block until as response is received. If the event loop is running, this method must have been called by a request or notification handler running on a greenlet. In that case, send the quest and yield to the parent greenlet until a response is available. When the event loop is not running, it will perform a blocking request like this: - Send the request - Run the loop until the response is available - Put requests/notifications received while waiting into a queue 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. """ async = kwargs.pop('async', False) if async: self._async_session.notify(method, args) return if kwargs: raise ValueError("request got unsupported keyword argument(s): {}" .format(', '.join(kwargs.keys()))) if self._is_running: v = self._yielding_request(method, args) else: v = self._blocking_request(method, args) if not v: # EOF raise IOError('EOF') err, rv = v if err: info("'Received error: %s", err) raise self.error_wrapper(err) return rv def run(self, request_cb, notification_cb, setup_cb=None): """Run the event loop to receive requests and notifications from Nvim. Like `AsyncSession.run()`, but `request_cb` and `notification_cb` are inside greenlets. """ self._request_cb = request_cb self._notification_cb = notification_cb self._is_running = True self._setup_exception = None def on_setup(): try: setup_cb() except Exception as e: self._setup_exception = e self.stop() if setup_cb: # Create a new greenlet to handle the setup function gr = greenlet.greenlet(on_setup) gr.switch() if self._setup_exception: error('Setup error: {}'.format(self._setup_exception)) raise self._setup_exception # Process all pending requests and notifications while self._pending_messages: msg = self._pending_messages.popleft() getattr(self, '_on_{}'.format(msg[0]))(*msg[1:]) self._async_session.run(self._on_request, self._on_notification) self._is_running = False self._request_cb = None self._notification_cb = None if self._setup_exception: raise self._setup_exception def stop(self): """Stop the event loop.""" self._async_session.stop() def _yielding_request(self, method, args): gr = greenlet.getcurrent() parent = gr.parent def response_cb(err, rv): debug('response is available for greenlet %s, switching back', gr) gr.switch(err, rv) self._async_session.request(method, args, response_cb) debug('yielding from greenlet %s to wait for response', gr) return parent.switch() def _blocking_request(self, method, args): result = [] def response_cb(err, rv): result.extend([err, rv]) self.stop() self._async_session.request(method, args, response_cb) self._async_session.run(self._enqueue_request, self._enqueue_notification) return result def _enqueue_request_and_stop(self, name, args, response): self._enqueue_request(name, args, response) self.stop() def _enqueue_notification_and_stop(self, name, args): self._enqueue_notification(name, args) self.stop() def _enqueue_request(self, name, args, response): self._pending_messages.append(('request', name, args, response,)) def _enqueue_notification(self, name, args): self._pending_messages.append(('notification', name, args,)) def _on_request(self, name, args, response): def handler(): try: rv = self._request_cb(name, args) debug('greenlet %s finished executing, ' + 'sending %s as response', gr, rv) response.send(rv) except ErrorResponse as err: warn("error response from request '%s %s': %s", name, args, format_exc()) response.send(err.args[0], error=True) except Exception as err: warn("error caught while processing request '%s %s': %s", name, args, format_exc()) response.send(repr(err) + "\n" + format_exc(5), error=True) debug('greenlet %s is now dying...', gr) # Create a new greenlet to handle the request gr = greenlet.greenlet(handler) debug('received rpc request, greenlet %s will handle it', gr) gr.switch() def _on_notification(self, name, args): def handler(): try: self._notification_cb(name, args) debug('greenlet %s finished executing', gr) except Exception: warn("error caught while processing notification '%s %s': %s", name, args, format_exc()) debug('greenlet %s is now dying...', gr) gr = greenlet.greenlet(handler) debug('received rpc notification, greenlet %s will handle it', gr) gr.switch() class ErrorResponse(BaseException): """Raise this in a request handler to respond with a given error message. Unlike when other exceptions are caught, this gives full control off the error response sent. When "ErrorResponse(msg)" is caught "msg" will be sent verbatim as the error response.No traceback will be appended. """ pass neovim-0.2.0/neovim/msgpack_rpc/__init__.py0000644000175000017500000000236312760330774021170 0ustar bjornbjorn00000000000000"""Msgpack-rpc subpackage. This package implements a msgpack-rpc client. While it was designed for handling some Nvim particularities(server->client requests for example), the code here should work with other msgpack-rpc servers. """ from .async_session import AsyncSession from .event_loop import EventLoop from .msgpack_stream import MsgpackStream from .session import ErrorResponse, Session __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session', 'ErrorResponse') def session(transport_type='stdio', *args, **kwargs): loop = EventLoop(transport_type, *args, **kwargs) msgpack_stream = MsgpackStream(loop) async_session = AsyncSession(msgpack_stream) session = Session(async_session) return session def tcp_session(address, port=7450): """Create a msgpack-rpc session from a tcp address/port.""" return session('tcp', address, port) def socket_session(path): """Create a msgpack-rpc session from a unix domain socket.""" return session('socket', path) def stdio_session(): """Create a msgpack-rpc session from stdin/stdout.""" return session('stdio') def child_session(argv): """Create a msgpack-rpc session from a new Nvim instance.""" return session('child', argv) neovim-0.2.0/neovim/msgpack_rpc/event_loop/0000755000175000017500000000000013200646543021217 5ustar bjornbjorn00000000000000neovim-0.2.0/neovim/msgpack_rpc/event_loop/__init__.py0000644000175000017500000000074112415724053023332 0ustar bjornbjorn00000000000000"""Event loop abstraction subpackage. Tries to use pyuv as a backend, falling back to the asyncio implementation. """ try: # libuv is fully implemented in C, use it when available from .uv import UvEventLoop EventLoop = UvEventLoop except ImportError: # asyncio(trollius on python 2) is pure python and should be more portable # across python implementations from .asyncio import AsyncioEventLoop EventLoop = AsyncioEventLoop __all__ = ('EventLoop') neovim-0.2.0/neovim/msgpack_rpc/event_loop/uv.py0000644000175000017500000000763213200643523022226 0ustar bjornbjorn00000000000000"""Event loop implementation that uses pyuv(libuv-python bindings).""" import sys from collections import deque import pyuv from .base import BaseEventLoop class UvEventLoop(BaseEventLoop): """`BaseEventLoop` subclass that uses `pvuv` as a backend.""" def _init(self): self._loop = pyuv.Loop() self._async = pyuv.Async(self._loop, self._on_async) self._connection_error = None self._error_stream = None self._callbacks = deque() def _on_connect(self, stream, error): self.stop() if error: msg = 'Cannot connect to {}: {}'.format( self._connect_address, pyuv.errno.strerror(error)) self._connection_error = IOError(msg) return self._read_stream = self._write_stream = stream def _on_read(self, handle, data, error): if error or not data: msg = pyuv.errno.strerror(error) if error else 'EOF' self._on_error(msg) return if handle == self._error_stream: return self._on_data(data) def _on_write(self, handle, error): if error: msg = pyuv.errno.strerror(error) self._on_error(msg) def _on_exit(self, handle, exit_status, term_signal): self._on_error('EOF') def _disconnected(self, *args): raise IOError('Not connected to Nvim') def _connect_tcp(self, address, port): stream = pyuv.TCP(self._loop) self._connect_address = '{}:{}'.format(address, port) stream.connect((address, port), self._on_connect) def _connect_socket(self, path): stream = pyuv.Pipe(self._loop) self._connect_address = path stream.connect(path, self._on_connect) def _connect_stdio(self): self._read_stream = pyuv.Pipe(self._loop) self._read_stream.open(sys.stdin.fileno()) self._write_stream = pyuv.Pipe(self._loop) self._write_stream.open(sys.stdout.fileno()) def _connect_child(self, argv): self._write_stream = pyuv.Pipe(self._loop) self._read_stream = pyuv.Pipe(self._loop) self._error_stream = pyuv.Pipe(self._loop) stdin = pyuv.StdIO(self._write_stream, flags=pyuv.UV_CREATE_PIPE + pyuv.UV_READABLE_PIPE) stdout = pyuv.StdIO(self._read_stream, flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE) stderr = pyuv.StdIO(self._error_stream, flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE) pyuv.Process.spawn(self._loop, args=argv, exit_callback=self._on_exit, flags=pyuv.UV_PROCESS_WINDOWS_HIDE, stdio=(stdin, stdout, stderr,)) self._error_stream.start_read(self._on_read) def _start_reading(self): if self._transport_type in ['tcp', 'socket']: self._loop.run() if self._connection_error: self.run = self.send = self._disconnected raise self._connection_error self._read_stream.start_read(self._on_read) def _send(self, data): self._write_stream.write(data, self._on_write) def _run(self): self._loop.run(pyuv.UV_RUN_DEFAULT) def _stop(self): self._loop.stop() def _threadsafe_call(self, fn): self._callbacks.append(fn) self._async.send() def _on_async(self, handle): while self._callbacks: self._callbacks.popleft()() def _setup_signals(self, signals): self._signal_handles = [] def handler(h, signum): self._on_signal(signum) for signum in signals: handle = pyuv.Signal(self._loop) handle.start(handler, signum) self._signal_handles.append(handle) def _teardown_signals(self): for handle in self._signal_handles: handle.stop() neovim-0.2.0/neovim/msgpack_rpc/event_loop/asyncio.py0000644000175000017500000001042213200643523023230 0ustar bjornbjorn00000000000000"""Event loop implementation that uses the `asyncio` standard module. The `asyncio` module was added to python standard library on 3.4, and it provides a pure python implementation of an event loop library. It is used as a fallback in case pyuv is not available(on python implementations other than CPython). Earlier python versions are supported through the `trollius` package, which is a backport of `asyncio` that works on Python 2.6+. """ from __future__ import absolute_import import os import sys from collections import deque try: # For python 3.4+, use the standard library module import asyncio except (ImportError, SyntaxError): # Fallback to trollius import trollius as asyncio from .base import BaseEventLoop loop_cls = asyncio.SelectorEventLoop if os.name == 'nt': # On windows use ProactorEventLoop which support pipes and is backed by the # more powerful IOCP facility loop_cls = asyncio.ProactorEventLoop class AsyncioEventLoop(BaseEventLoop, asyncio.Protocol, asyncio.SubprocessProtocol): """`BaseEventLoop` subclass that uses `asyncio` as a backend.""" def connection_made(self, transport): """Used to signal `asyncio.Protocol` of a successful connection.""" self._transport = transport if isinstance(transport, asyncio.SubprocessTransport): self._transport = transport.get_pipe_transport(0) def connection_lost(self, exc): """Used to signal `asyncio.Protocol` of a lost connection.""" self._on_error(exc.args[0] if exc else 'EOF') def data_received(self, data): """Used to signal `asyncio.Protocol` of incoming data.""" if self._on_data: self._on_data(data) return self._queued_data.append(data) def pipe_connection_lost(self, fd, exc): """Used to signal `asyncio.SubprocessProtocol` of a lost connection.""" self._on_error(exc.args[0] if exc else 'EOF') def pipe_data_received(self, fd, data): """Used to signal `asyncio.SubprocessProtocol` of incoming data.""" if fd == 2: # stderr fd number self._on_stderr(data) elif self._on_data: self._on_data(data) else: self._queued_data.append(data) def process_exited(self): """Used to signal `asyncio.SubprocessProtocol` when the child exits.""" self._on_error('EOF') def _init(self): self._loop = loop_cls() self._queued_data = deque() self._fact = lambda: self def _connect_tcp(self, address, port): coroutine = self._loop.create_connection(self._fact, address, port) self._loop.run_until_complete(coroutine) def _connect_socket(self, path): if os.name == 'nt': coroutine = self._loop.create_pipe_connection(self._fact, path) else: coroutine = self._loop.create_unix_connection(self._fact, path) self._loop.run_until_complete(coroutine) def _connect_stdio(self): coroutine = self._loop.connect_read_pipe(self._fact, sys.stdin) self._loop.run_until_complete(coroutine) coroutine = self._loop.connect_write_pipe(self._fact, sys.stdout) self._loop.run_until_complete(coroutine) def _connect_child(self, argv): self._child_watcher = asyncio.get_child_watcher() self._child_watcher.attach_loop(self._loop) coroutine = self._loop.subprocess_exec(self._fact, *argv) self._loop.run_until_complete(coroutine) def _start_reading(self): pass def _send(self, data): self._transport.write(data) def _run(self): while self._queued_data: self._on_data(self._queued_data.popleft()) self._loop.run_forever() def _stop(self): self._loop.stop() def _threadsafe_call(self, fn): self._loop.call_soon_threadsafe(fn) def _setup_signals(self, signals): if os.name == 'nt': # add_signal_handler is not supported in win32 self._signals = [] return self._signals = list(signals) for signum in self._signals: self._loop.add_signal_handler(signum, self._on_signal, signum) def _teardown_signals(self): for signum in self._signals: self._loop.remove_signal_handler(signum) neovim-0.2.0/neovim/msgpack_rpc/event_loop/base.py0000644000175000017500000001511513010315111022465 0ustar bjornbjorn00000000000000"""Common code for event loop implementations.""" import logging import signal import threading logger = logging.getLogger(__name__) debug, info, warn = (logger.debug, logger.info, logger.warning,) # When signals are restored, the event loop library may reset SIGINT to SIG_DFL # which exits the program. To be able to restore the python interpreter to it's # default state, we keep a reference to the default handler default_int_handler = signal.getsignal(signal.SIGINT) main_thread = threading.current_thread() class BaseEventLoop(object): """Abstract base class for all event loops. Event loops act as the bottom layer for Nvim sessions created by this library. They hide system/transport details behind a simple interface for reading/writing bytes to the connected Nvim instance. This class exposes public methods for interacting with the underlying event loop and delegates implementation-specific work to the following methods, which subclasses are expected to implement: - `_init()`: Implementation-specific initialization - `_connect_tcp(address, port)`: connect to Nvim using tcp/ip - `_connect_socket(path)`: Same as tcp, but use a UNIX domain socket or or named pipe. - `_connect_stdio()`: Use stdin/stdout as the connection to Nvim - `_connect_child(argv)`: Use the argument vector `argv` to spawn an embedded Nvim that has it's stdin/stdout connected to the event loop. - `_start_reading()`: Called after any of _connect_* methods. Can be used to perform any post-connection setup or validation. - `_send(data)`: Send `data`(byte array) to Nvim. The data is only - `_run()`: Runs the event loop until stopped or the connection is closed. calling the following methods when some event happens: actually sent when the event loop is running. - `_on_data(data)`: When Nvim sends some data. - `_on_signal(signum)`: When a signal is received. - `_on_error(message)`: When a non-recoverable error occurs(eg: connection lost) - `_stop()`: Stop the event loop - `_interrupt(data)`: Like `stop()`, but may be called from other threads this. - `_setup_signals(signals)`: Add implementation-specific listeners for for `signals`, which is a list of OS-specific signal numbers. - `_teardown_signals()`: Removes signal listeners set by `_setup_signals` """ def __init__(self, transport_type, *args): """Initialize and connect the event loop instance. The only arguments are the transport type and transport-specific configuration, like this: >>> BaseEventLoop('tcp', '127.0.0.1', 7450) Traceback (most recent call last): ... AttributeError: 'BaseEventLoop' object has no attribute '_init' >>> BaseEventLoop('socket', '/tmp/nvim-socket') Traceback (most recent call last): ... AttributeError: 'BaseEventLoop' object has no attribute '_init' >>> BaseEventLoop('stdio') Traceback (most recent call last): ... AttributeError: 'BaseEventLoop' object has no attribute '_init' >>> BaseEventLoop('child', ['nvim', '--embed', '-u', 'NONE']) Traceback (most recent call last): ... AttributeError: 'BaseEventLoop' object has no attribute '_init' This calls the implementation-specific initialization `_init`, one of the `_connect_*` methods(based on `transport_type`) and `_start_reading()` """ self._transport_type = transport_type self._signames = dict((k, v) for v, k in signal.__dict__.items() if v.startswith('SIG')) self._on_data = None self._error = None self._init() getattr(self, '_connect_{}'.format(transport_type))(*args) self._start_reading() def connect_tcp(self, address, port): """Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`.""" info('Connecting to TCP address: %s:%d', address, port) self._connect_tcp(address, port) def connect_socket(self, path): """Connect to socket at `path`. Delegated to `_connect_socket`.""" info('Connecting to %s', path) self._connect_socket(path) def connect_stdio(self): """Connect using stdin/stdout. Delegated to `_connect_stdio`.""" info('Preparing stdin/stdout for streaming data') self._connect_stdio() def connect_child(self, argv): """Connect a new Nvim instance. Delegated to `_connect_child`.""" info('Spawning a new nvim instance') self._connect_child(argv) def send(self, data): """Queue `data` for sending to Nvim.""" debug("Sending '%s'", data) self._send(data) def threadsafe_call(self, fn): """Call a function in the event loop thread. This is the only safe way to interact with a session from other threads. """ self._threadsafe_call(fn) def run(self, data_cb): """Run the event loop.""" if self._error: err = self._error if isinstance(self._error, KeyboardInterrupt): # KeyboardInterrupt is not destructive(it may be used in # the REPL). # After throwing KeyboardInterrupt, cleanup the _error field # so the loop may be started again self._error = None raise err self._on_data = data_cb if threading.current_thread() == main_thread: self._setup_signals([signal.SIGINT, signal.SIGTERM]) debug('Entering event loop') self._run() debug('Exited event loop') if threading.current_thread() == main_thread: self._teardown_signals() signal.signal(signal.SIGINT, default_int_handler) self._on_data = None def stop(self): """Stop the event loop.""" self._stop() debug('Stopped event loop') def _on_signal(self, signum): msg = 'Received {}'.format(self._signames[signum]) debug(msg) if signum == signal.SIGINT and self._transport_type == 'stdio': # When the transport is stdio, we are probably running as a Nvim # child process. In that case, we don't want to be killed by # ctrl+C return cls = Exception if signum == signal.SIGINT: cls = KeyboardInterrupt self._error = cls(msg) self.stop() def _on_error(self, error): debug(error) self._error = IOError(error) self.stop() def _on_interrupt(self): self.stop() neovim-0.2.0/neovim/msgpack_rpc/async_session.py0000644000175000017500000001102512761561140022276 0ustar bjornbjorn00000000000000"""Asynchronous msgpack-rpc handling in the event loop pipeline.""" import logging from traceback import format_exc logger = logging.getLogger(__name__) debug, info, warn = (logger.debug, logger.info, logger.warning,) class AsyncSession(object): """Asynchronous msgpack-rpc layer that wraps a msgpack stream. This wraps the msgpack stream interface for reading/writing msgpack documents and exposes an interface for sending and receiving msgpack-rpc requests and notifications. """ def __init__(self, msgpack_stream): """Wrap `msgpack_stream` on a msgpack-rpc interface.""" self._msgpack_stream = msgpack_stream self._next_request_id = 1 self._pending_requests = {} self._request_cb = self._notification_cb = None self._handlers = { 0: self._on_request, 1: self._on_response, 2: self._on_notification } def threadsafe_call(self, fn): """Wrapper around `MsgpackStream.threadsafe_call`.""" self._msgpack_stream.threadsafe_call(fn) def request(self, method, args, response_cb): """Send a msgpack-rpc request to Nvim. A msgpack-rpc with method `method` and argument `args` is sent to Nvim. The `response_cb` function is called with when the response is available. """ request_id = self._next_request_id self._next_request_id = request_id + 1 self._msgpack_stream.send([0, request_id, method, args]) self._pending_requests[request_id] = response_cb def notify(self, method, args): """Send a msgpack-rpc notification to Nvim. A msgpack-rpc with method `method` and argument `args` is sent to Nvim. This will have the same effect as a request, but no response will be recieved """ self._msgpack_stream.send([2, method, args]) def run(self, request_cb, notification_cb): """Run the event loop to receive requests and notifications from Nvim. While the event loop is running, `request_cb` and `_notification_cb` will be called whenever requests or notifications are respectively available. """ self._request_cb = request_cb self._notification_cb = notification_cb self._msgpack_stream.run(self._on_message) self._request_cb = None self._notification_cb = None def stop(self): """Stop the event loop.""" self._msgpack_stream.stop() def _on_message(self, msg): try: self._handlers.get(msg[0], self._on_invalid_message)(msg) except Exception: err_str = format_exc(5) warn(err_str) self._msgpack_stream.send([1, 0, err_str, None]) def _on_request(self, msg): # request # - msg[1]: id # - msg[2]: method name # - msg[3]: arguments debug('received request: %s, %s', msg[2], msg[3]) self._request_cb(msg[2], msg[3], Response(self._msgpack_stream, msg[1])) def _on_response(self, msg): # response to a previous request: # - msg[1]: the id # - msg[2]: error(if any) # - msg[3]: result(if not errored) debug('received response: %s, %s', msg[2], msg[3]) self._pending_requests.pop(msg[1])(msg[2], msg[3]) def _on_notification(self, msg): # notification/event # - msg[1]: event name # - msg[2]: arguments debug('received notification: %s, %s', msg[1], msg[2]) self._notification_cb(msg[1], msg[2]) def _on_invalid_message(self, msg): error = 'Received invalid message %s' % msg warn(error) self._msgpack_stream.send([1, 0, error, None]) class Response(object): """Response to a msgpack-rpc request that came from Nvim. When Nvim sends a msgpack-rpc request, an instance of this class is created for remembering state required to send a response. """ def __init__(self, msgpack_stream, request_id): """Initialize the Response instance.""" self._msgpack_stream = msgpack_stream self._request_id = request_id def send(self, value, error=False): """Send the response. If `error` is True, it will be sent as an error. """ if error: resp = [1, self._request_id, value, None] else: resp = [1, self._request_id, None, value] debug('sending response to request %d: %s', self._request_id, resp) self._msgpack_stream.send(resp) neovim-0.2.0/neovim/msgpack_rpc/msgpack_stream.py0000644000175000017500000000357312770005312022421 0ustar bjornbjorn00000000000000"""Msgpack handling in the event loop pipeline.""" import logging from msgpack import Packer, Unpacker from ..compat import unicode_errors_default logger = logging.getLogger(__name__) debug, info, warn = (logger.debug, logger.info, logger.warning,) class MsgpackStream(object): """Two-way msgpack stream that wraps a event loop byte stream. This wraps the event loop interface for reading/writing bytes and exposes an interface for reading/writing msgpack documents. """ def __init__(self, event_loop): """Wrap `event_loop` on a msgpack-aware interface.""" self._event_loop = event_loop self._packer = Packer(unicode_errors=unicode_errors_default) self._unpacker = Unpacker() self._message_cb = None def threadsafe_call(self, fn): """Wrapper around `BaseEventLoop.threadsafe_call`.""" self._event_loop.threadsafe_call(fn) def send(self, msg): """Queue `msg` for sending to Nvim.""" debug('sent %s', msg) self._event_loop.send(self._packer.pack(msg)) def run(self, message_cb): """Run the event loop to receive messages from Nvim. While the event loop is running, `message_cb` will be called whenever a message has been successfully parsed from the input stream. """ self._message_cb = message_cb self._event_loop.run(self._on_data) self._message_cb = None def stop(self): """Stop the event loop.""" self._event_loop.stop() def _on_data(self, data): self._unpacker.feed(data) while True: try: debug('waiting for message...') msg = next(self._unpacker) debug('received message: %s', msg) self._message_cb(msg) except StopIteration: debug('unpacker needs more data...') break neovim-0.2.0/neovim/plugin/0000755000175000017500000000000013200646543016052 5ustar bjornbjorn00000000000000neovim-0.2.0/neovim/plugin/host.py0000644000175000017500000002032613200645644017405 0ustar bjornbjorn00000000000000"""Implements a Nvim host for python plugins.""" import imp import inspect import logging import os import os.path import re from functools import partial from traceback import format_exc from . import script_host from ..api import decode_if_bytes, walk from ..compat import IS_PYTHON3, find_module from ..msgpack_rpc import ErrorResponse from ..util import format_exc_skip __all__ = ('Host') logger = logging.getLogger(__name__) error, debug, info, warn = (logger.error, logger.debug, logger.info, logger.warning,) class Host(object): """Nvim host for python plugins. Takes care of loading/unloading plugins and routing msgpack-rpc requests/notifications to the appropriate handlers. """ def __init__(self, nvim): """Set handlers for plugin_load/plugin_unload.""" self.nvim = nvim self._specs = {} self._loaded = {} self._load_errors = {} self._notification_handlers = {} self._request_handlers = { 'poll': lambda: 'ok', 'specs': self._on_specs_request, 'shutdown': self.shutdown } # Decode per default for Python3 self._decode_default = IS_PYTHON3 def _on_async_err(self, msg): self.nvim.err_write(msg, async=True) def start(self, plugins): """Start listening for msgpack-rpc requests and notifications.""" self.nvim.run_loop(self._on_request, self._on_notification, lambda: self._load(plugins), err_cb=self._on_async_err) def shutdown(self): """Shutdown the host.""" self._unload() self.nvim.stop_loop() def _wrap_function(self, fn, sync, decode, nvim_bind, name, *args): if decode: args = walk(decode_if_bytes, args, decode) if nvim_bind is not None: args.insert(0, nvim_bind) try: return fn(*args) except Exception: if sync: msg = ("error caught in request handler '{} {}':\n{}" .format(name, args, format_exc_skip(1))) raise ErrorResponse(msg) else: msg = ("error caught in async handler '{} {}'\n{}\n" .format(name, args, format_exc_skip(1))) self._on_async_err(msg + "\n") def _on_request(self, name, args): """Handle a msgpack-rpc request.""" if IS_PYTHON3: name = decode_if_bytes(name) handler = self._request_handlers.get(name, None) if not handler: msg = self._missing_handler_error(name, 'request') error(msg) raise ErrorResponse(msg) debug('calling request handler for "%s", args: "%s"', name, args) rv = handler(*args) debug("request handler for '%s %s' returns: %s", name, args, rv) return rv def _on_notification(self, name, args): """Handle a msgpack-rpc notification.""" if IS_PYTHON3: name = decode_if_bytes(name) handler = self._notification_handlers.get(name, None) if not handler: msg = self._missing_handler_error(name, 'notification') error(msg) self._on_async_err(msg + "\n") return debug('calling notification handler for "%s", args: "%s"', name, args) handler(*args) def _missing_handler_error(self, name, kind): msg = 'no {} handler registered for "{}"'.format(kind, name) pathmatch = re.match(r'(.+):[^:]+:[^:]+', name) if pathmatch: loader_error = self._load_errors.get(pathmatch.group(1)) if loader_error is not None: msg = msg + "\n" + loader_error return msg def _load(self, plugins): for path in plugins: err = None if path in self._loaded: error('{} is already loaded'.format(path)) continue try: if path == "script_host.py": module = script_host else: directory, name = os.path.split(os.path.splitext(path)[0]) file, pathname, descr = find_module(name, [directory]) module = imp.load_module(name, file, pathname, descr) handlers = [] self._discover_classes(module, handlers, path) self._discover_functions(module, handlers, path) if not handlers: error('{} exports no handlers'.format(path)) continue self._loaded[path] = {'handlers': handlers, 'module': module} except Exception as e: err = ('Encountered {} loading plugin at {}: {}\n{}' .format(type(e).__name__, path, e, format_exc(5))) error(err) self._load_errors[path] = err def _unload(self): for path, plugin in self._loaded.items(): handlers = plugin['handlers'] for handler in handlers: method_name = handler._nvim_rpc_method_name if hasattr(handler, '_nvim_shutdown_hook'): handler() elif handler._nvim_rpc_sync: del self._request_handlers[method_name] else: del self._notification_handlers[method_name] self._specs = {} self._loaded = {} def _discover_classes(self, module, handlers, plugin_path): for _, cls in inspect.getmembers(module, inspect.isclass): if getattr(cls, '_nvim_plugin', False): # create an instance of the plugin and pass the nvim object plugin = cls(self._configure_nvim_for(cls)) # discover handlers in the plugin instance self._discover_functions(plugin, handlers, plugin_path) def _discover_functions(self, obj, handlers, plugin_path): def predicate(o): return hasattr(o, '_nvim_rpc_method_name') specs = [] objdecode = getattr(obj, '_nvim_decode', self._decode_default) for _, fn in inspect.getmembers(obj, predicate): sync = fn._nvim_rpc_sync decode = getattr(fn, '_nvim_decode', objdecode) nvim_bind = None if fn._nvim_bind: nvim_bind = self._configure_nvim_for(fn) method = fn._nvim_rpc_method_name if fn._nvim_prefix_plugin_path: method = '{}:{}'.format(plugin_path, method) fn_wrapped = partial(self._wrap_function, fn, sync, decode, nvim_bind, method) self._copy_attributes(fn, fn_wrapped) # register in the rpc handler dict if sync: if method in self._request_handlers: raise Exception(('Request handler for "{}" is ' + 'already registered').format(method)) self._request_handlers[method] = fn_wrapped else: if method in self._notification_handlers: raise Exception(('Notification handler for "{}" is ' + 'already registered').format(method)) self._notification_handlers[method] = fn_wrapped if hasattr(fn, '_nvim_rpc_spec'): specs.append(fn._nvim_rpc_spec) handlers.append(fn_wrapped) if specs: self._specs[plugin_path] = specs def _copy_attributes(self, fn, fn2): # Copy _nvim_* attributes from the original function for attr in dir(fn): if attr.startswith('_nvim_'): setattr(fn2, attr, getattr(fn, attr)) def _on_specs_request(self, path): if IS_PYTHON3: path = decode_if_bytes(path) if path in self._load_errors: self.nvim.out_write(self._load_errors[path] + '\n') return self._specs.get(path, 0) def _configure_nvim_for(self, obj): # Configure a nvim instance for obj (checks encoding configuration) nvim = self.nvim decode = getattr(obj, '_nvim_decode', self._decode_default) if decode: nvim = nvim.with_decode(decode) return nvim neovim-0.2.0/neovim/plugin/script_host.py0000644000175000017500000002031313200640620020752 0ustar bjornbjorn00000000000000"""Legacy python/python3-vim emulation.""" import imp import io import logging import os import sys from .decorators import plugin, rpc_export from ..api import Nvim, walk from ..msgpack_rpc import ErrorResponse from ..util import format_exc_skip __all__ = ('ScriptHost',) logger = logging.getLogger(__name__) debug, info, warn = (logger.debug, logger.info, logger.warn,) IS_PYTHON3 = sys.version_info >= (3, 0) if IS_PYTHON3: basestring = str if sys.version_info >= (3, 4): from importlib.machinery import PathFinder @plugin class ScriptHost(object): """Provides an environment for running python plugins created for Vim.""" def __init__(self, nvim): """Initialize the legacy python-vim environment.""" self.setup(nvim) # context where all code will run self.module = imp.new_module('__main__') nvim.script_context = self.module # it seems some plugins assume 'sys' is already imported, so do it now exec('import sys', self.module.__dict__) self.legacy_vim = LegacyVim.from_nvim(nvim) sys.modules['vim'] = self.legacy_vim def setup(self, nvim): """Setup import hooks and global streams. This will add import hooks for importing modules from runtime directories and patch the sys module so 'print' calls will be forwarded to Nvim. """ self.nvim = nvim info('install import hook/path') self.hook = path_hook(nvim) sys.path_hooks.append(self.hook) nvim.VIM_SPECIAL_PATH = '_vim_path_' sys.path.append(nvim.VIM_SPECIAL_PATH) info('redirect sys.stdout and sys.stderr') self.saved_stdout = sys.stdout self.saved_stderr = sys.stderr sys.stdout = RedirectStream(lambda data: nvim.out_write(data)) sys.stderr = RedirectStream(lambda data: nvim.err_write(data)) def teardown(self): """Restore state modified from the `setup` call.""" nvim = self.nvim info('uninstall import hook/path') sys.path.remove(nvim.VIM_SPECIAL_PATH) sys.path_hooks.remove(self.hook) info('restore sys.stdout and sys.stderr') sys.stdout = self.saved_stdout sys.stderr = self.saved_stderr @rpc_export('python_execute', sync=True) def python_execute(self, script, range_start, range_stop): """Handle the `python` ex command.""" self._set_current_range(range_start, range_stop) try: exec(script, self.module.__dict__) except Exception: raise ErrorResponse(format_exc_skip(1)) @rpc_export('python_execute_file', sync=True) def python_execute_file(self, file_path, range_start, range_stop): """Handle the `pyfile` ex command.""" self._set_current_range(range_start, range_stop) with open(file_path) as f: script = compile(f.read(), file_path, 'exec') try: exec(script, self.module.__dict__) except Exception: raise ErrorResponse(format_exc_skip(1)) @rpc_export('python_do_range', sync=True) def python_do_range(self, start, stop, code): """Handle the `pydo` ex command.""" self._set_current_range(start, stop) nvim = self.nvim start -= 1 fname = '_vim_pydo' # define the function function_def = 'def %s(line, linenr):\n %s' % (fname, code,) exec(function_def, self.module.__dict__) # get the function function = self.module.__dict__[fname] while start < stop: # Process batches of 5000 to avoid the overhead of making multiple # API calls for every line. Assuming an average line length of 100 # bytes, approximately 488 kilobytes will be transferred per batch, # which can be done very quickly in a single API call. sstart = start sstop = min(start + 5000, stop) lines = nvim.current.buffer.api.get_lines(sstart, sstop, True) exception = None newlines = [] linenr = sstart + 1 for i, line in enumerate(lines): result = function(line, linenr) if result is None: # Update earlier lines, and skip to the next if newlines: end = sstart + len(newlines) - 1 nvim.current.buffer.api.set_lines(sstart, end, True, newlines) sstart += len(newlines) + 1 newlines = [] pass elif isinstance(result, basestring): newlines.append(result) else: exception = TypeError('pydo should return a string ' + 'or None, found %s instead' % result.__class__.__name__) break linenr += 1 start = sstop if newlines: end = sstart + len(newlines) nvim.current.buffer.api.set_lines(sstart, end, True, newlines) if exception: raise exception # delete the function del self.module.__dict__[fname] @rpc_export('python_eval', sync=True) def python_eval(self, expr): """Handle the `pyeval` vim function.""" return eval(expr, self.module.__dict__) def _set_current_range(self, start, stop): current = self.legacy_vim.current current.range = current.buffer.range(start, stop) class RedirectStream(io.IOBase): def __init__(self, redirect_handler): self.redirect_handler = redirect_handler def write(self, data): self.redirect_handler(data) def writelines(self, seq): self.redirect_handler('\n'.join(seq)) if IS_PYTHON3: num_types = (int, float) else: num_types = (int, long, float) def num_to_str(obj): if isinstance(obj, num_types): return str(obj) else: return obj class LegacyVim(Nvim): def eval(self, expr): obj = self.request("vim_eval", expr) return walk(num_to_str, obj) # This was copied/adapted from nvim-python help def path_hook(nvim): def _get_paths(): return discover_runtime_directories(nvim) def _find_module(fullname, oldtail, path): idx = oldtail.find('.') if idx > 0: name = oldtail[:idx] tail = oldtail[idx + 1:] fmr = imp.find_module(name, path) module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr) return _find_module(fullname, tail, module.__path__) else: return imp.find_module(fullname, path) class VimModuleLoader(object): def __init__(self, module): self.module = module def load_module(self, fullname, path=None): # Check sys.modules, required for reload (see PEP302). if fullname in sys.modules: return sys.modules[fullname] return imp.load_module(fullname, *self.module) class VimPathFinder(object): @staticmethod def find_module(fullname, path=None): """Method for Python 2.7 and 3.3.""" try: return VimModuleLoader( _find_module(fullname, fullname, path or _get_paths())) except ImportError: return None @staticmethod def find_spec(fullname, path=None, target=None): """Method for Python 3.4+.""" return PathFinder.find_spec(fullname, path or _get_paths(), target) def hook(path): if path == nvim.VIM_SPECIAL_PATH: return VimPathFinder else: raise ImportError return hook def discover_runtime_directories(nvim): rv = [] for path in nvim.list_runtime_paths(): if not os.path.exists(path): continue path1 = os.path.join(path, 'pythonx') if IS_PYTHON3: path2 = os.path.join(path, 'python3') else: path2 = os.path.join(path, 'python2') if os.path.exists(path1): rv.append(path1) if os.path.exists(path2): rv.append(path2) return rv neovim-0.2.0/neovim/plugin/__init__.py0000644000175000017500000000047712770005312020165 0ustar bjornbjorn00000000000000"""Nvim plugin/host subpackage.""" from .decorators import (autocmd, command, decode, encoding, function, plugin, rpc_export, shutdown_hook) from .host import Host __all__ = ('Host', 'plugin', 'rpc_export', 'command', 'autocmd', 'function', 'encoding', 'decode', 'shutdown_hook') neovim-0.2.0/neovim/plugin/decorators.py0000644000175000017500000001100413200645644020566 0ustar bjornbjorn00000000000000"""Decorators used by python host plugin system.""" import inspect import logging from ..compat import IS_PYTHON3, unicode_errors_default logger = logging.getLogger(__name__) debug, info, warn = (logger.debug, logger.info, logger.warning,) __all__ = ('plugin', 'rpc_export', 'command', 'autocmd', 'function', 'encoding', 'decode', 'shutdown_hook') def plugin(cls): """Tag a class as a plugin. This decorator is required to make the a class methods discoverable by the plugin_load method of the host. """ cls._nvim_plugin = True # the _nvim_bind attribute is set to True by default, meaning that # decorated functions have a bound Nvim instance as first argument. # For methods in a plugin-decorated class this is not required, because # the class initializer will already receive the nvim object. predicate = lambda fn: hasattr(fn, '_nvim_bind') for _, fn in inspect.getmembers(cls, predicate): if IS_PYTHON3: fn._nvim_bind = False else: fn.im_func._nvim_bind = False return cls def rpc_export(rpc_method_name, sync=False): """Export a function or plugin method as a msgpack-rpc request handler.""" def dec(f): f._nvim_rpc_method_name = rpc_method_name f._nvim_rpc_sync = sync f._nvim_bind = True f._nvim_prefix_plugin_path = False return f return dec def command(name, nargs=0, complete=None, range=None, count=None, bang=False, register=False, sync=False, allow_nested=False, eval=None): """Tag a function or plugin method as a Nvim command handler.""" def dec(f): f._nvim_rpc_method_name = 'command:{}'.format(name) f._nvim_rpc_sync = sync f._nvim_bind = True f._nvim_prefix_plugin_path = True opts = {} if range is not None: opts['range'] = '' if range is True else str(range) elif count: opts['count'] = count if bang: opts['bang'] = '' if register: opts['register'] = '' if nargs: opts['nargs'] = nargs if complete: opts['complete'] = complete if eval: opts['eval'] = eval if not sync and allow_nested: rpc_sync = "urgent" else: rpc_sync = sync f._nvim_rpc_spec = { 'type': 'command', 'name': name, 'sync': rpc_sync, 'opts': opts } return f return dec def autocmd(name, pattern='*', sync=False, allow_nested=False, eval=None): """Tag a function or plugin method as a Nvim autocommand handler.""" def dec(f): f._nvim_rpc_method_name = 'autocmd:{}:{}'.format(name, pattern) f._nvim_rpc_sync = sync f._nvim_bind = True f._nvim_prefix_plugin_path = True opts = { 'pattern': pattern } if eval: opts['eval'] = eval if not sync and allow_nested: rpc_sync = "urgent" else: rpc_sync = sync f._nvim_rpc_spec = { 'type': 'autocmd', 'name': name, 'sync': rpc_sync, 'opts': opts } return f return dec def function(name, range=False, sync=False, allow_nested=False, eval=None): """Tag a function or plugin method as a Nvim function handler.""" def dec(f): f._nvim_rpc_method_name = 'function:{}'.format(name) f._nvim_rpc_sync = sync f._nvim_bind = True f._nvim_prefix_plugin_path = True opts = {} if range: opts['range'] = '' if range is True else str(range) if eval: opts['eval'] = eval if not sync and allow_nested: rpc_sync = "urgent" else: rpc_sync = sync f._nvim_rpc_spec = { 'type': 'function', 'name': name, 'sync': rpc_sync, 'opts': opts } return f return dec def shutdown_hook(f): """Tag a function or method as a shutdown hook.""" f._nvim_shutdown_hook = True f._nvim_bind = True return f def decode(mode=unicode_errors_default): """Configure automatic encoding/decoding of strings.""" def dec(f): f._nvim_decode = mode return f return dec def encoding(encoding=True): """DEPRECATED: use neovim.decode().""" if isinstance(encoding, str): encoding = True def dec(f): f._nvim_decode = encoding return f return dec neovim-0.2.0/neovim/__init__.py0000644000175000017500000001163713200646412016670 0ustar bjornbjorn00000000000000"""Python client for Nvim. Client library for talking with Nvim processes via it's msgpack-rpc API. """ import logging import os import sys from .api import Nvim from .compat import IS_PYTHON3 from .msgpack_rpc import (ErrorResponse, child_session, socket_session, stdio_session, tcp_session) from .plugin import (Host, autocmd, command, decode, encoding, function, plugin, rpc_export, shutdown_hook) from .util import Version __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session', 'start_host', 'autocmd', 'command', 'encoding', 'decode', 'function', 'plugin', 'rpc_export', 'Host', 'Nvim', 'VERSION', 'shutdown_hook', 'attach', 'setup_logging', 'ErrorResponse') VERSION = Version(major=0, minor=2, patch=0, prerelease='') def start_host(session=None): """Promote the current process into python plugin host for Nvim. Start msgpack-rpc event loop for `session`, listening for Nvim requests and notifications. It registers Nvim commands for loading/unloading python plugins. The sys.stdout and sys.stderr streams are redirected to Nvim through `session`. That means print statements probably won't work as expected while this function doesn't return. This function is normally called at program startup and could have been defined as a separate executable. It is exposed as a library function for testing purposes only. """ plugins = [] for arg in sys.argv: _, ext = os.path.splitext(arg) if ext == '.py': plugins.append(arg) elif os.path.isdir(arg): init = os.path.join(arg, '__init__.py') if os.path.isfile(init): plugins.append(arg) # This is a special case to support the old workaround of # adding an empty .py file to make a package directory # visible, and it should be removed soon. for path in list(plugins): dup = path + ".py" if os.path.isdir(path) and dup in plugins: plugins.remove(dup) # Special case: the legacy scripthost receives a single relative filename # while the rplugin host will receive absolute paths. if plugins == ["script_host.py"]: name = "script" else: name = "rplugin" setup_logging(name) if not session: session = stdio_session() nvim = Nvim.from_session(session) if nvim.version.api_level < 1: sys.stderr.write("This version of the neovim python package " "requires nvim 0.1.6 or later") sys.exit(1) host = Host(nvim) host.start(plugins) def attach(session_type, address=None, port=None, path=None, argv=None, decode=None): """Provide a nicer interface to create python api sessions. Previous machinery to create python api sessions is still there. This only creates a facade function to make things easier for the most usual cases. Thus, instead of: from neovim import socket_session, Nvim session = tcp_session(address=
, port=) nvim = Nvim.from_session(session) You can now do: from neovim import attach nvim = attach('tcp', address=
, port=) And also: nvim = attach('socket', path=) nvim = attach('child', argv=) nvim = attach('stdio') """ session = (tcp_session(address, port) if session_type == 'tcp' else socket_session(path) if session_type == 'socket' else stdio_session() if session_type == 'stdio' else child_session(argv) if session_type == 'child' else None) if not session: raise Exception('Unknown session type "%s"' % session_type) if decode is None: decode = IS_PYTHON3 return Nvim.from_session(session).with_decode(decode) def setup_logging(name): """Setup logging according to environment variables.""" logger = logging.getLogger(__name__) if 'NVIM_PYTHON_LOG_FILE' in os.environ: prefix = os.environ['NVIM_PYTHON_LOG_FILE'].strip() major_version = sys.version_info[0] logfile = '{}_py{}_{}'.format(prefix, major_version, name) handler = logging.FileHandler(logfile, 'w', 'utf-8') handler.formatter = logging.Formatter( '%(asctime)s [%(levelname)s @ ' '%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s') logging.root.addHandler(handler) level = logging.INFO if 'NVIM_PYTHON_LOG_LEVEL' in os.environ: lvl = getattr(logging, os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(), level) if isinstance(lvl, int): level = lvl logger.setLevel(level) # Required for python 2.6 class NullHandler(logging.Handler): def emit(self, record): pass if not logging.root.handlers: logging.root.addHandler(NullHandler()) neovim-0.2.0/neovim/api/0000755000175000017500000000000013200646543015325 5ustar bjornbjorn00000000000000neovim-0.2.0/neovim/api/common.py0000644000175000017500000001360013014014356017161 0ustar bjornbjorn00000000000000"""Code shared between the API classes.""" import functools from msgpack import unpackb from ..compat import unicode_errors_default class Remote(object): """Base class for Nvim objects(buffer/window/tabpage). Each type of object has it's own specialized class with API wrappers around the msgpack-rpc session. This implements equality which takes the remote object handle into consideration. """ def __init__(self, session, code_data): """Initialize from session and code_data immutable object. The `code_data` contains serialization information required for msgpack-rpc calls. It must be immutable for Buffer equality to work. """ self._session = session self.code_data = code_data self.handle = unpackb(code_data[1]) self.api = RemoteApi(self, self._api_prefix) self.vars = RemoteMap(self, self._api_prefix + 'get_var', self._api_prefix + 'set_var') self.options = RemoteMap(self, self._api_prefix + 'get_option', self._api_prefix + 'set_option') def __eq__(self, other): """Return True if `self` and `other` are the same object.""" return (hasattr(other, 'code_data') and other.code_data == self.code_data) def __hash__(self): """Return hash based on remote object id.""" return self.code_data.__hash__() def request(self, name, *args, **kwargs): """Wrapper for nvim.request.""" return self._session.request(name, self, *args, **kwargs) class RemoteApi(object): """Wrapper to allow api methods to be called like python methods.""" def __init__(self, obj, api_prefix): """Initialize a RemoteApi with object and api prefix.""" self._obj = obj self._api_prefix = api_prefix def __getattr__(self, name): """Return wrapper to named api method.""" return functools.partial(self._obj.request, self._api_prefix + name) class RemoteMap(object): """Represents a string->object map stored in Nvim. This is the dict counterpart to the `RemoteSequence` class, but it is used as a generic way of retrieving values from the various map-like data structures present in Nvim. It is used to provide a dict-like API to vim variables and options. """ def __init__(self, obj, get_method, set_method=None, self_obj=None): """Initialize a RemoteMap with session, getter/setter and self_obj.""" self._get = functools.partial(obj.request, get_method) self._set = None if set_method: self._set = functools.partial(obj.request, set_method) def __getitem__(self, key): """Return a map value by key.""" return self._get(key) def __setitem__(self, key, value): """Set a map value by key(if the setter was provided).""" if not self._set: raise TypeError('This dict is read-only') self._set(key, value) def __delitem__(self, key): """Delete a map value by associating None with the key.""" if not self._set: raise TypeError('This dict is read-only') return self._set(key, None) def __contains__(self, key): """Check if key is present in the map.""" try: self._get(key) return True except Exception: return False def get(self, key, default=None): """Return value for key if present, else a default value.""" try: return self._get(key) except Exception: return default class RemoteSequence(object): """Represents a sequence of objects stored in Nvim. This class is used to wrap msgapck-rpc functions that work on Nvim sequences(of lines, buffers, windows and tabpages) with an API that is similar to the one provided by the python-vim interface. For example, the 'windows' property of the `Nvim` class is a RemoteSequence sequence instance, and the expression `nvim.windows[0]` is translated to session.request('vim_get_windows')[0]. It can also receive an optional self_obj that will be passed as first argument of the request. For example, `tabpage.windows[0]` is translated to: session.request('tabpage_get_windows', tabpage_instance)[0]. One important detail about this class is that all methods will fetch the sequence into a list and perform the necessary manipulation locally(iteration, indexing, counting, etc). """ def __init__(self, session, method): """Initialize a RemoteSequence with session, method and self_obj.""" self._fetch = functools.partial(session.request, method) def __len__(self): """Return the length of the remote sequence.""" return len(self._fetch()) def __getitem__(self, idx): """Return a sequence item by index.""" if not isinstance(idx, slice): return self._fetch()[idx] return self._fetch()[idx.start:idx.stop] def __iter__(self): """Return an iterator for the sequence.""" items = self._fetch() for item in items: yield item def __contains__(self, item): """Check if an item is present in the sequence.""" return item in self._fetch() def _identity(obj, session, method, kind): return obj def decode_if_bytes(obj, mode=True): """Decode obj if it is bytes.""" if mode is True: mode = unicode_errors_default if isinstance(obj, bytes): return obj.decode("utf-8", errors=mode) return obj def walk(fn, obj, *args, **kwargs): """Recursively walk an object graph applying `fn`/`args` to objects.""" if type(obj) in [list, tuple]: return list(walk(fn, o, *args) for o in obj) if type(obj) is dict: return dict((walk(fn, k, *args), walk(fn, v, *args)) for k, v in obj.items()) return fn(obj, *args, **kwargs) neovim-0.2.0/neovim/api/window.py0000644000175000017500000000366513010304721017204 0ustar bjornbjorn00000000000000"""API for working with Nvim windows.""" from .common import Remote __all__ = ('Window') class Window(Remote): """A remote Nvim window.""" _api_prefix = "nvim_win_" @property def buffer(self): """Get the `Buffer` currently being displayed by the window.""" return self.request('nvim_win_get_buf') @property def cursor(self): """Get the (row, col) tuple with the current cursor position.""" return self.request('nvim_win_get_cursor') @cursor.setter def cursor(self, pos): """Set the (row, col) tuple as the new cursor position.""" return self.request('nvim_win_set_cursor', pos) @property def height(self): """Get the window height in rows.""" return self.request('nvim_win_get_height') @height.setter def height(self, height): """Set the window height in rows.""" return self.request('nvim_win_set_height', height) @property def width(self): """Get the window width in rows.""" return self.request('nvim_win_get_width') @width.setter def width(self, width): """Set the window height in rows.""" return self.request('nvim_win_set_width', width) @property def row(self): """0-indexed, on-screen window position(row) in display cells.""" return self.request('nvim_win_get_position')[0] @property def col(self): """0-indexed, on-screen window position(col) in display cells.""" return self.request('nvim_win_get_position')[1] @property def tabpage(self): """Get the `Tabpage` that contains the window.""" return self.request('nvim_win_get_tabpage') @property def valid(self): """Return True if the window still exists.""" return self.request('nvim_win_is_valid') @property def number(self): """Get the window number.""" return self.request('nvim_win_get_number') neovim-0.2.0/neovim/api/tabpage.py0000644000175000017500000000175613010304721017277 0ustar bjornbjorn00000000000000"""API for working with Nvim tabpages.""" from .common import Remote, RemoteSequence __all__ = ('Tabpage') class Tabpage(Remote): """A remote Nvim tabpage.""" _api_prefix = "nvim_tabpage_" def __init__(self, *args): """Initialize from session and code_data immutable object. The `code_data` contains serialization information required for msgpack-rpc calls. It must be immutable for Buffer equality to work. """ super(Tabpage, self).__init__(*args) self.windows = RemoteSequence(self, 'nvim_tabpage_list_wins') @property def window(self): """Get the `Window` currently focused on the tabpage.""" return self.request('nvim_tabpage_get_win') @property def valid(self): """Return True if the tabpage still exists.""" return self.request('nvim_tabpage_is_valid') @property def number(self): """Get the tabpage number.""" return self.request('nvim_tabpage_get_number') neovim-0.2.0/neovim/api/__init__.py0000644000175000017500000000057312770005312017435 0ustar bjornbjorn00000000000000"""Nvim API subpackage. This package implements a higher-level API that wraps msgpack-rpc `Session` instances. """ from .buffer import Buffer from .common import decode_if_bytes, walk from .nvim import Nvim, NvimError from .tabpage import Tabpage from .window import Window __all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError', 'decode_if_bytes', 'walk') neovim-0.2.0/neovim/api/nvim.py0000644000175000017500000003561713200645644016665 0ustar bjornbjorn00000000000000"""Main Nvim interface.""" import functools import os import sys from traceback import format_stack from msgpack import ExtType from .buffer import Buffer from .common import (Remote, RemoteApi, RemoteMap, RemoteSequence, decode_if_bytes, walk) from .tabpage import Tabpage from .window import Window from ..compat import IS_PYTHON3 from ..util import Version, format_exc_skip __all__ = ('Nvim') os_chdir = os.chdir 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. """ @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(e[1]) channel_id, metadata = session.request(b'vim_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') self.vvars = RemoteMap(self, 'nvim_get_vvar', 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.error = NvimError self._decode = decode self._err_cb = err_cb 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 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. """ 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 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): """Register as a remote UI. After this method is called, the client will receive redraw notifications. """ return self.request('ui_attach', width, height, rgb) def ui_detach(self): """Unregister as a remote UI.""" return self.request('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 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: -> '\x1b' -> '\r' -> '\x0c' -> '\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): """Print `msg` as a normal message.""" return self.request('nvim_out_write', msg, **kwargs) def err_write(self, msg, **kwargs): """Print `msg` as an error message.""" return self.request('nvim_err_write', msg, **kwargs) 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 IOError: # 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) @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 functools.partial(self._nvim.call, name) class NvimError(Exception): pass neovim-0.2.0/neovim/api/buffer.py0000644000175000017500000001324013200643523017143 0ustar bjornbjorn00000000000000"""API for working with a Nvim Buffer.""" from .common import Remote from ..compat import IS_PYTHON3 __all__ = ('Buffer') if IS_PYTHON3: basestring = str def adjust_index(idx, default=None): """Convert from python indexing convention to nvim indexing convention.""" if idx is None: return default elif idx < 0: return idx - 1 else: return idx class Buffer(Remote): """A remote Nvim buffer.""" _api_prefix = "nvim_buf_" def __len__(self): """Return the number of lines contained in a Buffer.""" return self.request('nvim_buf_line_count') def __getitem__(self, idx): """Get a buffer line or slice by integer index. Indexes may be negative to specify positions from the end of the buffer. For example, -1 is the last line, -2 is the line before that and so on. When retrieving slices, omiting indexes(eg: `buffer[:]`) will bring the whole buffer. """ if not isinstance(idx, slice): i = adjust_index(idx) return self.request('nvim_buf_get_lines', i, i + 1, True)[0] start = adjust_index(idx.start, 0) end = adjust_index(idx.stop, -1) return self.request('nvim_buf_get_lines', start, end, False) def __setitem__(self, idx, item): """Replace a buffer line or slice by integer index. Like with `__getitem__`, indexes may be negative. When replacing slices, omiting indexes(eg: `buffer[:]`) will replace the whole buffer. """ if not isinstance(idx, slice): i = adjust_index(idx) lines = [item] if item is not None else [] return self.request('nvim_buf_set_lines', i, i + 1, True, lines) lines = item if item is not None else [] start = adjust_index(idx.start, 0) end = adjust_index(idx.stop, -1) return self.request('nvim_buf_set_lines', start, end, False, lines) def __iter__(self): """Iterate lines of a buffer. This will retrieve all lines locally before iteration starts. This approach is used because for most cases, the gain is much greater by minimizing the number of API calls by transfering all data needed to work. """ lines = self[:] for line in lines: yield line def __delitem__(self, idx): """Delete line or slice of lines from the buffer. This is the same as __setitem__(idx, []) """ self.__setitem__(idx, None) def append(self, lines, index=-1): """Append a string or list of lines to the buffer.""" if isinstance(lines, (basestring, bytes)): lines = [lines] return self.request('nvim_buf_set_lines', index, index, True, lines) def mark(self, name): """Return (row, col) tuple for a named mark.""" return self.request('nvim_buf_get_mark', name) def range(self, start, end): """Return a `Range` object, which represents part of the Buffer.""" return Range(self, start, end) def add_highlight(self, hl_group, line, col_start=0, col_end=-1, src_id=-1, async=None): """Add a highlight to the buffer.""" if async is None: async = (src_id != 0) return self.request('nvim_buf_add_highlight', src_id, hl_group, line, col_start, col_end, async=async) def clear_highlight(self, src_id, line_start=0, line_end=-1, async=True): """Clear highlights from the buffer.""" self.request('nvim_buf_clear_highlight', src_id, line_start, line_end, async=async) @property def name(self): """Get the buffer name.""" return self.request('nvim_buf_get_name') @name.setter def name(self, value): """Set the buffer name. BufFilePre/BufFilePost are triggered.""" return self.request('nvim_buf_set_name', value) @property def valid(self): """Return True if the buffer still exists.""" return self.request('nvim_buf_is_valid') @property def number(self): """Get the buffer number.""" return self.handle class Range(object): def __init__(self, buffer, start, end): self._buffer = buffer self.start = start - 1 self.end = end - 1 def __len__(self): return self.end - self.start + 1 def __getitem__(self, idx): if not isinstance(idx, slice): return self._buffer[self._normalize_index(idx)] start = self._normalize_index(idx.start) end = self._normalize_index(idx.stop) if start is None: start = self.start if end is None: end = self.end + 1 return self._buffer[start:end] def __setitem__(self, idx, lines): if not isinstance(idx, slice): self._buffer[self._normalize_index(idx)] = lines return start = self._normalize_index(idx.start) end = self._normalize_index(idx.stop) if start is None: start = self.start if end is None: end = self.end self._buffer[start:end + 1] = lines def __iter__(self): for i in range(self.start, self.end + 1): yield self._buffer[i] def append(self, lines, i=None): i = self._normalize_index(i) if i is None: i = self.end + 1 self._buffer.append(lines, i) def _normalize_index(self, index): if index is None: return None if index < 0: index = self.end else: index += self.start if index > self.end: index = self.end return index neovim-0.2.0/neovim/version.py0000644000175000017500000000016313200641402016600 0ustar bjornbjorn00000000000000from .util import Version VERSION = Version(major=0, minor=1, patch=14, prerelease="dev") __all__ = ('VERSION',) neovim-0.2.0/neovim/util.py0000644000175000017500000000166713111562407016112 0ustar bjornbjorn00000000000000"""Shared utility functions.""" import sys from traceback import format_exception def format_exc_skip(skip, limit=None): """Like traceback.format_exc but allow skipping the first frames.""" type, val, tb = sys.exc_info() for i in range(skip): tb = tb.tb_next return ('\n'.join(format_exception(type, val, tb, limit))).rstrip() # Taken from SimpleNamespace in python 3 class Version: """Helper class for version info.""" def __init__(self, **kwargs): """Create the Version object.""" self.__dict__.update(kwargs) def __repr__(self): """Return str representation of the Version.""" keys = sorted(self.__dict__) items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) return "{}({})".format(type(self).__name__, ", ".join(items)) def __eq__(self, other): """Check if version is same as other.""" return self.__dict__ == other.__dict__ neovim-0.2.0/neovim/compat.py0000644000175000017500000000212112770005312016377 0ustar bjornbjorn00000000000000"""Code for supporting compatibility across python versions.""" import sys from imp import find_module as original_find_module IS_PYTHON3 = sys.version_info >= (3, 0) if IS_PYTHON3: def find_module(fullname, path): """Compatibility wrapper for imp.find_module. Automatically decodes arguments of find_module, in Python3 they must be Unicode """ if isinstance(fullname, bytes): fullname = fullname.decode() if isinstance(path, bytes): path = path.decode() elif isinstance(path, list): newpath = [] for element in path: if isinstance(element, bytes): newpath.append(element.decode()) else: newpath.append(element) path = newpath return original_find_module(fullname, path) # There is no 'long' type in Python3 just int long = int unicode_errors_default = 'surrogateescape' else: find_module = original_find_module unicode_errors_default = 'strict' NUM_TYPES = (int, long, float) neovim-0.2.0/MANIFEST.in0000644000175000017500000000003212760330774015016 0ustar bjornbjorn00000000000000include README.md LICENSE neovim-0.2.0/README.md0000644000175000017500000001651313200645644014545 0ustar bjornbjorn00000000000000### Python client to [Neovim](https://github.com/neovim/neovim) [![Build Status](https://travis-ci.org/neovim/python-client.svg?branch=master)](https://travis-ci.org/neovim/python-client) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/neovim/python-client/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/neovim/python-client/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/neovim/python-client/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/neovim/python-client/?branch=master) Implements support for python plugins in Nvim. Also works as a library for connecting to and scripting Nvim processes through its msgpack-rpc API. #### Installation Supports python 2.7, and 3.3 or later. ```sh pip2 install neovim pip3 install neovim ``` If you only use one of python2 or python3, it is enough to install that version. You can install the package without being root by adding the `--user` flag. If you follow Neovim master, make sure to upgrade the python-client when you upgrade neovim: ```sh pip2 install --upgrade neovim pip3 install --upgrade neovim ``` Alternatively, the master version could be installed by executing the following in the root of this repository: ```sh pip2 install . pip3 install . ``` #### Python Plugin API Neovim has a new mechanism for defining plugins, as well as a number of extensions to the python API. The API extensions are accessible no matter if the traditional `:python` interface or the new mechanism is used, as discussed below. * `vim.funcs` exposes vimscript functions (both builtin and global user defined functions) as a python namespace. For instance to set the value of a register ``` vim.funcs.setreg('0', ["some", "text"], 'l') ``` * `vim.api` exposes nvim API methods. For instance to call `nvim_strwidth`, ``` result = vim.api.strwidth("some text") ``` Note the initial `nvim_` is not included. Also, object methods can be called directly on their object, ``` buf = vim.current.buffer len = buf.api.line_count() ``` calls `nvim_buf_line_count`. Alternatively msgpack requests can be invoked directly, ``` result = vim.request("nvim_strwith", "some text") len = vim.request("nvim_buf_line_count", buf) ``` * The API is not thread-safe in general. However, `vim.async_call` allows a spawned thread to schedule code to be executed on the main thread. This method could also be called from `:python` or a synchronous request handler, to defer some execution that shouldn't block nvim. ``` :python vim.async_call(myfunc, args...) ``` Note that this code will still block the plugin host if it does long-running computations. Intensive computations should be done in a separate thread (or process), and `vim.async_call` can be used to send results back to nvim. * Some methods accept an `async` keyword argument: `vim.eval`, `vim.command`, `vim.request` as well as the `vim.funcs` and `vim.api` wrappers. When `async=True` is passed the client will not wait for nvim to complete the request (which also means that the return value is unavailable). #### Remote (new-style) plugins Neovim allows python plugins to be defined by placing python files or packages in `rplugin/python3/` (in a runtimepath folder). These follow the structure of this example: ```python import neovim @neovim.plugin class TestPlugin(object): def __init__(self, nvim): self.nvim = nvim @neovim.function("TestFunction", sync=True) def testfunction(self, args): return 3 @neovim.command("TestCommand", range='', nargs='*') def testcommand(self, args, range): self.nvim.current.line = ('Command with args: {}, range: {}' .format(args, range)) @neovim.autocmd('BufEnter', pattern='*.py', eval='expand("")', sync=True) def on_bufenter(self, filename): self.nvim.out_write("testplugin is in " + filename + "\n") ``` If `sync=True` is supplied nvim will wait for the handler to finish (this is required for function return values), but by default handlers are executed asynchronously. Normally async handlers (`sync=False`, the default) are blocked while a synchronous handler is running. This ensures that async handlers can call requests without nvim confusing these requests with requests from a synchronous handler. To execute an asynchronous handler even when other handlers are running, add `allow_nested=True` to the decorator. The handler must then not make synchronous nvim requests, but it can make asynchronous requests, i e passing `async=True`. You need to run `:UpdateRemotePlugins` in nvim for changes in the specifications to have effect. For details see `:help remote-plugin` in nvim. #### Development If you change the code, you need to run ```sh pip2 install . pip3 install . ``` for the changes to have effect. Alternatively you could execute neovim with the `$PYTHONPATH` environment variable ``` PYTHONPATH=/path/to/python-client nvim ``` But note this is not completely reliable as installed packages can appear before `$PYTHONPATH` in the python search path. You need to rerun this command if you have changed the code, in order for nvim to use it for the plugin host. To run the tests execute ```sh nosetests ``` This will run the tests in an embedded instance of nvim. If you want to test a different version than `nvim` in `$PATH` use ```sh NVIM_CHILD_ARGV='["/path/to/nvim", "-u", "NONE", "--embed"]' nosetests ``` Alternatively, if you want to see the state of nvim, you could use ```sh export NVIM_LISTEN_ADDRESS=/tmp/nvimtest xterm -e "nvim -u NONE"& nosetests ``` But note you need to restart nvim every time you run the tests! Substitute your favorite terminal emulator for `xterm`. #### Troubleshooting You can run the plugin host in nvim with logging enabled to debug errors: ``` NVIM_PYTHON_LOG_FILE=logfile NVIM_PYTHON_LOG_LEVEL=DEBUG nvim ``` As more than one python host process might be started, the log filenames take the pattern `logfile_pyX_KIND` where `X` is the major python version (2 or 3) and `KIND` is either "rplugin" or "script" (for the `:python[3]` script interface). If the host cannot start at all, the error could be found in `~/.nvimlog` if `nvim` was compiled with logging. #### Usage through the python REPL A number of different transports are supported, but the simplest way to get started is with the python REPL. First, start Nvim with a known address (or use the `$NVIM_LISTEN_ADDRESS` of a running instance): ```sh $ NVIM_LISTEN_ADDRESS=/tmp/nvim nvim ``` In another terminal, connect a python REPL to Nvim (note that the API is similar to the one exposed by the [python-vim bridge](http://vimdoc.sourceforge.net/htmldoc/if_pyth.html#python-vim)): ```python >>> from neovim import attach # Create a python API session attached to unix domain socket created above: >>> nvim = attach('socket', path='/tmp/nvim') # Now do some work. >>> buffer = nvim.current.buffer # Get the current buffer >>> buffer[0] = 'replace first line' >>> buffer[:] = ['replace whole buffer'] >>> nvim.command('vsplit') >>> nvim.windows[1].width = 10 >>> nvim.vars['global_var'] = [1, 2, 3] >>> nvim.eval('g:global_var') [1, 2, 3] ``` You can embed neovim into your python application instead of binding to a running neovim instance. ```python >>> from neovim import attach >>> nvim = attach('child', argv=["/bin/env", "nvim", "--embed"]) ``` The tests can be consulted for more examples. neovim-0.2.0/setup.cfg0000644000175000017500000000011513200646543015075 0ustar bjornbjorn00000000000000[flake8] ignore = D211,E731,F821,D401 [egg_info] tag_build = tag_date = 0 neovim-0.2.0/test/0000755000175000017500000000000013200646543014236 5ustar bjornbjorn00000000000000neovim-0.2.0/test/test_client_rpc.py0000644000175000017500000000433112770005223017765 0ustar bjornbjorn00000000000000# -*- coding: utf-8 -*- from nose.tools import with_setup, eq_ as eq from test_common import vim, cleanup cid = vim.channel_id @with_setup(setup=cleanup) def test_call_and_reply(): def setup_cb(): cmd = 'let g:result = rpcrequest(%d, "client-call", 1, 2, 3)' % cid vim.command(cmd) eq(vim.vars['result'], [4, 5, 6]) vim.stop_loop() def request_cb(name, args): eq(name, 'client-call') eq(args, [1, 2, 3]) return [4, 5, 6] vim.run_loop(request_cb, None, setup_cb) @with_setup(setup=cleanup) def test_call_api_before_reply(): def setup_cb(): cmd = 'let g:result = rpcrequest(%d, "client-call2", 1, 2, 3)' % cid vim.command(cmd) eq(vim.vars['result'], [7, 8, 9]) vim.stop_loop() def request_cb(name, args): vim.command('let g:result2 = [7, 8, 9]') return vim.vars['result2'] vim.run_loop(request_cb, None, setup_cb) @with_setup(setup=cleanup) def test_async_call(): def request_cb(name, args): if name == "test-event": vim.vars['result'] = 17 vim.stop_loop() # this would have dead-locked if not async vim.funcs.rpcrequest(vim.channel_id, "test-event", async=True) vim.run_loop(request_cb, None, None) eq(vim.vars['result'], 17) @with_setup(setup=cleanup) def test_recursion(): def setup_cb(): vim.vars['result1'] = 0 vim.vars['result2'] = 0 vim.vars['result3'] = 0 vim.vars['result4'] = 0 cmd = 'let g:result1 = rpcrequest(%d, "call", %d)' % (cid, 2,) vim.command(cmd) eq(vim.vars['result1'], 4) eq(vim.vars['result2'], 8) eq(vim.vars['result3'], 16) eq(vim.vars['result4'], 32) vim.stop_loop() def request_cb(name, args): n = args[0] n *= 2 if n <= 16: if n == 4: cmd = 'let g:result2 = rpcrequest(%d, "call", %d)' % (cid, n,) elif n == 8: cmd = 'let g:result3 = rpcrequest(%d, "call", %d)' % (cid, n,) elif n == 16: cmd = 'let g:result4 = rpcrequest(%d, "call", %d)' % (cid, n,) vim.command(cmd) return n vim.run_loop(request_cb, None, setup_cb) neovim-0.2.0/test/test_tabpage.py0000644000175000017500000000210313010304721017232 0ustar bjornbjorn00000000000000import os from nose.tools import with_setup, eq_ as eq, ok_ as ok from test_common import vim, cleanup @with_setup(setup=cleanup) def test_windows(): vim.command('tabnew') vim.command('vsplit') eq(list(vim.tabpages[0].windows), [vim.windows[0]]) eq(list(vim.tabpages[1].windows), [vim.windows[1], vim.windows[2]]) eq(vim.tabpages[1].window, vim.windows[1]) vim.current.window = vim.windows[2] eq(vim.tabpages[1].window, vim.windows[2]) @with_setup(setup=cleanup) def test_vars(): vim.current.tabpage.vars['python'] = [1, 2, {'3': 1}] eq(vim.current.tabpage.vars['python'], [1, 2, {'3': 1}]) eq(vim.eval('t:python'), [1, 2, {'3': 1}]) @with_setup(setup=cleanup) def test_valid(): vim.command('tabnew') tabpage = vim.tabpages[1] ok(tabpage.valid) vim.command('tabclose') ok(not tabpage.valid) @with_setup(setup=cleanup) def test_number(): curnum = vim.current.tabpage.number vim.command('tabnew') eq(vim.current.tabpage.number, curnum + 1) vim.command('tabnew') eq(vim.current.tabpage.number, curnum + 2) neovim-0.2.0/test/test_concurrency.py0000644000175000017500000000136112760330774020210 0ustar bjornbjorn00000000000000from nose.tools import with_setup, eq_ as eq from test_common import vim, cleanup from threading import Timer @with_setup(setup=cleanup) def test_interrupt_from_another_thread(): timer = Timer(0.5, lambda: vim.async_call(lambda: vim.stop_loop())) timer.start() eq(vim.next_message(), None) @with_setup(setup=cleanup) def test_exception_in_threadsafe_call(): # an exception in a threadsafe_call shouldn't crash the entire host msgs = [] vim.async_call(lambda: [vim.eval("3"), undefined_variable]) timer = Timer(0.5, lambda: vim.async_call(lambda: vim.stop_loop())) timer.start() vim.run_loop(None, None, err_cb=msgs.append) eq(len(msgs), 1) msgs[0].index('NameError') msgs[0].index('undefined_variable') neovim-0.2.0/test/test_vim.py0000644000175000017500000001073213200643523016440 0ustar bjornbjorn00000000000000# -*- coding: utf-8 -*- import os, tempfile from nose.tools import with_setup, eq_ as eq, ok_ as ok from test_common import vim, cleanup def source(code): fd, fname = tempfile.mkstemp() with os.fdopen(fd,'w') as f: f.write(code) vim.command('source '+fname) os.unlink(fname) @with_setup(setup=cleanup) def test_command(): fname = tempfile.mkstemp()[1] vim.command('new') vim.command('edit %s' % fname) # skip the "press return" state, which does not handle deferred calls vim.input('\r') vim.command('normal itesting\npython\napi') vim.command('w') ok(os.path.isfile(fname)) with open(fname) as f: eq(f.read(), 'testing\npython\napi\n') os.unlink(fname) @with_setup def test_command_output(): eq(vim.command_output('echo test'), 'test') @with_setup(setup=cleanup) def test_eval(): vim.command('let g:v1 = "a"') vim.command('let g:v2 = [1, 2, {"v3": 3}]') eq(vim.eval('g:'), {'v1': 'a', 'v2': [1, 2, {'v3': 3}]}) @with_setup(setup=cleanup) def test_call(): eq(vim.funcs.join(['first', 'last'], ', '), 'first, last') source(""" function! Testfun(a,b) return string(a:a).":".a:b endfunction """) eq(vim.funcs.Testfun(3, 'alpha'), '3:alpha') @with_setup(setup=cleanup) def test_api(): vim.api.command('let g:var = 3') eq(vim.api.eval('g:var'), 3) @with_setup(setup=cleanup) def test_strwidth(): eq(vim.strwidth('abc'), 3) # 6 + (neovim) # 19 * 2 (each japanese character occupies two cells) eq(vim.strwidth('neovimのデザインかなりまともなのになってる。'), 44) @with_setup(setup=cleanup) def test_chdir(): pwd = vim.eval('getcwd()') root = os.path.abspath(os.sep) # We can chdir to '/' on Windows, but then the pwd will be the root drive vim.chdir('/') eq(vim.eval('getcwd()'), root) vim.chdir(pwd) eq(vim.eval('getcwd()'), pwd) @with_setup(setup=cleanup) def test_current_line(): eq(vim.current.line, '') vim.current.line = 'abc' eq(vim.current.line, 'abc') @with_setup(setup=cleanup) def test_vars(): vim.vars['python'] = [1, 2, {'3': 1}] eq(vim.vars['python'], [1, 2, {'3': 1}]) eq(vim.eval('g:python'), [1, 2, {'3': 1}]) @with_setup(setup=cleanup) def test_options(): eq(vim.options['listchars'], 'tab:> ,trail:-,nbsp:+') vim.options['listchars'] = 'tab:xy' eq(vim.options['listchars'], 'tab:xy') @with_setup(setup=cleanup) def test_buffers(): buffers = [] # Number of elements eq(len(vim.buffers), 1) # Indexing (by buffer number) eq(vim.buffers[vim.current.buffer.number], vim.current.buffer) buffers.append(vim.current.buffer) vim.command('new') eq(len(vim.buffers), 2) buffers.append(vim.current.buffer) eq(vim.buffers[vim.current.buffer.number], vim.current.buffer) vim.current.buffer = buffers[0] eq(vim.buffers[vim.current.buffer.number], buffers[0]) # Membership test ok(buffers[0] in vim.buffers) ok(buffers[1] in vim.buffers) ok({} not in vim.buffers) # Iteration eq(buffers, list(vim.buffers)) @with_setup(setup=cleanup) def test_windows(): eq(len(vim.windows), 1) eq(vim.windows[0], vim.current.window) vim.command('vsplit') vim.command('split') eq(len(vim.windows), 3) eq(vim.windows[0], vim.current.window) vim.current.window = vim.windows[1] eq(vim.windows[1], vim.current.window) @with_setup(setup=cleanup) def test_tabpages(): eq(len(vim.tabpages), 1) eq(vim.tabpages[0], vim.current.tabpage) vim.command('tabnew') eq(len(vim.tabpages), 2) eq(len(vim.windows), 2) eq(vim.windows[1], vim.current.window) eq(vim.tabpages[1], vim.current.tabpage) vim.current.window = vim.windows[0] # Switching window also switches tabpages if necessary(this probably # isn't the current behavior, but compatibility will be handled in the # python client with an optional parameter) eq(vim.tabpages[0], vim.current.tabpage) eq(vim.windows[0], vim.current.window) vim.current.tabpage = vim.tabpages[1] eq(vim.tabpages[1], vim.current.tabpage) eq(vim.windows[1], vim.current.window) @with_setup(setup=cleanup) def test_hash(): d = {} d[vim.current.buffer] = "alpha" eq(d[vim.current.buffer], "alpha") vim.command('new') d[vim.current.buffer] = "beta" eq(d[vim.current.buffer], "beta") vim.command('winc w') eq(d[vim.current.buffer], "alpha") vim.command('winc w') eq(d[vim.current.buffer], "beta") neovim-0.2.0/test/test_events.py0000644000175000017500000000322512760330774017163 0ustar bjornbjorn00000000000000# -*- coding: utf-8 -*- from nose.tools import with_setup, eq_ as eq from test_common import vim, cleanup @with_setup(setup=cleanup) def test_receiving_events(): vim.command('call rpcnotify(%d, "test-event", 1, 2, 3)' % vim.channel_id) event = vim.next_message() eq(event[1], 'test-event') eq(event[2], [1, 2, 3]) vim.command('au FileType python call rpcnotify(%d, "py!", bufnr("$"))' % vim.channel_id) vim.command('set filetype=python') event = vim.next_message() eq(event[1], 'py!') eq(event[2], [vim.current.buffer.number]) @with_setup(setup=cleanup) def test_sending_notify(): # notify after notify vim.command("let g:test = 3", async=True) cmd = 'call rpcnotify(%d, "test-event", g:test)' % vim.channel_id vim.command(cmd, async=True) event = vim.next_message() eq(event[1], 'test-event') eq(event[2], [3]) # request after notify vim.command("let g:data = 'xyz'", async=True) eq(vim.eval('g:data'), 'xyz') @with_setup(setup=cleanup) def test_broadcast(): vim.subscribe('event2') vim.command('call rpcnotify(0, "event1", 1, 2, 3)') vim.command('call rpcnotify(0, "event2", 4, 5, 6)') vim.command('call rpcnotify(0, "event2", 7, 8, 9)') event = vim.next_message() eq(event[1], 'event2') eq(event[2], [4, 5, 6]) event = vim.next_message() eq(event[1], 'event2') eq(event[2], [7, 8, 9]) vim.unsubscribe('event2') vim.subscribe('event1') vim.command('call rpcnotify(0, "event2", 10, 11, 12)') vim.command('call rpcnotify(0, "event1", 13, 14, 15)') msg = vim.next_message() eq(msg[1], 'event1') eq(msg[2], [13, 14, 15]) neovim-0.2.0/test/test_common.py0000644000175000017500000000252113003441047017130 0ustar bjornbjorn00000000000000import json import os import sys import neovim from nose.tools import eq_ as eq neovim.setup_logging("test") child_argv = os.environ.get('NVIM_CHILD_ARGV') listen_address = os.environ.get('NVIM_LISTEN_ADDRESS') if child_argv is None and listen_address is None: child_argv = '["nvim", "-u", "NONE", "--embed"]' if child_argv is not None: vim = neovim.attach('child', argv=json.loads(child_argv)) else: vim = neovim.attach('socket', path=listen_address) cleanup_func = ''':function BeforeEachTest() set all& redir => groups silent augroup redir END for group in split(groups) exe 'augroup '.group autocmd! augroup END endfor autocmd! tabnew let curbufnum = eval(bufnr('%')) redir => buflist silent ls! redir END let bufnums = [] for buf in split(buflist, '\\n') let bufnum = eval(split(buf, '[ u]')[0]) if bufnum != curbufnum call add(bufnums, bufnum) endif endfor if len(bufnums) > 0 exe 'silent bwipeout! '.join(bufnums, ' ') endif silent tabonly for k in keys(g:) exe 'unlet g:'.k endfor filetype plugin indent off mapclear mapclear! abclear comclear endfunction ''' vim.input(cleanup_func) def cleanup(): # cleanup nvim vim.command('call BeforeEachTest()') eq(len(vim.tabpages), 1) eq(len(vim.windows), 1) eq(len(vim.buffers), 1) neovim-0.2.0/test/test_buffer.py0000644000175000017500000001310713200643523017115 0ustar bjornbjorn00000000000000import os from nose.tools import with_setup, eq_ as eq, ok_ as ok from test_common import vim, cleanup from neovim.compat import IS_PYTHON3 @with_setup(setup=cleanup) def test_get_length(): eq(len(vim.current.buffer), 1) vim.current.buffer.append('line') eq(len(vim.current.buffer), 2) vim.current.buffer.append('line') eq(len(vim.current.buffer), 3) vim.current.buffer[-1] = None eq(len(vim.current.buffer), 2) vim.current.buffer[-1] = None vim.current.buffer[-1] = None # There's always at least one line eq(len(vim.current.buffer), 1) @with_setup(setup=cleanup) def test_get_set_del_line(): eq(vim.current.buffer[0], '') vim.current.buffer[0] = 'line1' eq(vim.current.buffer[0], 'line1') vim.current.buffer[0] = 'line2' eq(vim.current.buffer[0], 'line2') vim.current.buffer[0] = None eq(vim.current.buffer[0], '') # __delitem__ vim.current.buffer[:] = ['line1', 'line2', 'line3'] eq(vim.current.buffer[2], 'line3') del vim.current.buffer[0] eq(vim.current.buffer[0], 'line2') eq(vim.current.buffer[1], 'line3') del vim.current.buffer[-1] eq(vim.current.buffer[0], 'line2') eq(len(vim.current.buffer), 1) @with_setup(setup=cleanup) def test_get_set_del_slice(): eq(vim.current.buffer[:], ['']) # Replace buffer vim.current.buffer[:] = ['a', 'b', 'c'] eq(vim.current.buffer[:], ['a', 'b', 'c']) eq(vim.current.buffer[1:], ['b', 'c']) eq(vim.current.buffer[1:2], ['b']) eq(vim.current.buffer[1:1], []) eq(vim.current.buffer[:-1], ['a', 'b']) eq(vim.current.buffer[1:-1], ['b']) eq(vim.current.buffer[-2:], ['b', 'c']) vim.current.buffer[1:2] = ['a', 'b', 'c'] eq(vim.current.buffer[:], ['a', 'a', 'b', 'c', 'c']) vim.current.buffer[-1:] = ['a', 'b', 'c'] eq(vim.current.buffer[:], ['a', 'a', 'b', 'c', 'a', 'b', 'c']) vim.current.buffer[:-3] = None eq(vim.current.buffer[:], ['a', 'b', 'c']) vim.current.buffer[:] = None eq(vim.current.buffer[:], ['']) # __delitem__ vim.current.buffer[:] = ['a', 'b', 'c'] del vim.current.buffer[:] eq(vim.current.buffer[:], ['']) vim.current.buffer[:] = ['a', 'b', 'c'] del vim.current.buffer[:1] eq(vim.current.buffer[:], ['b', 'c']) del vim.current.buffer[:-1] eq(vim.current.buffer[:], ['c']) @with_setup(setup=cleanup) def test_vars(): vim.current.buffer.vars['python'] = [1, 2, {'3': 1}] eq(vim.current.buffer.vars['python'], [1, 2, {'3': 1}]) eq(vim.eval('b:python'), [1, 2, {'3': 1}]) @with_setup(setup=cleanup) def test_api(): vim.current.buffer.api.set_var('myvar', 'thetext') eq(vim.current.buffer.api.get_var('myvar'), 'thetext') eq(vim.eval('b:myvar'), 'thetext') vim.current.buffer.api.set_lines(0,-1,True,['alpha', 'beta']) eq(vim.current.buffer.api.get_lines(0,-1,True), ['alpha', 'beta']) eq(vim.current.buffer[:], ['alpha', 'beta']) @with_setup(setup=cleanup) def test_options(): eq(vim.current.buffer.options['shiftwidth'], 8) vim.current.buffer.options['shiftwidth'] = 4 eq(vim.current.buffer.options['shiftwidth'], 4) # global-local option vim.current.buffer.options['define'] = 'test' eq(vim.current.buffer.options['define'], 'test') # Doesn't change the global value eq(vim.options['define'], '^\s*#\s*define') @with_setup(setup=cleanup) def test_number(): curnum = vim.current.buffer.number vim.command('new') eq(vim.current.buffer.number, curnum + 1) vim.command('new') eq(vim.current.buffer.number, curnum + 2) @with_setup(setup=cleanup) def test_name(): vim.command('new') eq(vim.current.buffer.name, '') new_name = vim.eval('resolve(tempname())') vim.current.buffer.name = new_name eq(vim.current.buffer.name, new_name) vim.command('silent w!') ok(os.path.isfile(new_name)) os.unlink(new_name) @with_setup(setup=cleanup) def test_valid(): vim.command('new') buffer = vim.current.buffer ok(buffer.valid) vim.command('bw!') ok(not buffer.valid) @with_setup(setup=cleanup) def test_append(): vim.current.buffer.append('a') eq(vim.current.buffer[:], ['', 'a']) vim.current.buffer.append('b', 0) eq(vim.current.buffer[:], ['b', '', 'a']) vim.current.buffer.append(['c', 'd']) eq(vim.current.buffer[:], ['b', '', 'a', 'c', 'd']) vim.current.buffer.append(['c', 'd'], 2) eq(vim.current.buffer[:], ['b', '', 'c', 'd', 'a', 'c', 'd']) vim.current.buffer.append(b'bytes') eq(vim.current.buffer[:], ['b', '', 'c', 'd', 'a', 'c', 'd', 'bytes']) @with_setup(setup=cleanup) def test_mark(): vim.current.buffer.append(['a', 'bit of', 'text']) vim.current.window.cursor = [3, 4] vim.command('mark V') eq(vim.current.buffer.mark('V'), [3, 0]) @with_setup(setup=cleanup) def test_invalid_utf8(): vim.command('normal "=printf("%c", 0xFF)\np') eq(vim.eval("char2nr(getline(1))"), 0xFF) eq(vim.current.buffer[:], ['\udcff'] if IS_PYTHON3 else ['\xff']) vim.current.line += 'x' eq(vim.eval("getline(1)", decode=False), b'\xFFx') eq(vim.current.buffer[:], ['\udcffx'] if IS_PYTHON3 else ['\xffx']) @with_setup(setup=cleanup) def test_get_exceptions(): try: vim.current.buffer.options['invalid-option'] ok(False) except vim.error: pass @with_setup(setup=cleanup) def test_contains(): ok(vim.current.buffer in vim.buffers) @with_setup(setup=cleanup) def test_set_items_for_range(): vim.current.buffer[:] = ['a', 'b', 'c', 'd', 'e'] r = vim.current.buffer.range(1, 3) r[1:3] = ['foo']*3 eq(vim.current.buffer[:], ['a', 'foo', 'foo', 'foo', 'd', 'e']) neovim-0.2.0/test/test_window.py0000644000175000017500000000653113014014356017155 0ustar bjornbjorn00000000000000import os from nose.tools import with_setup, eq_ as eq, ok_ as ok from test_common import vim, cleanup @with_setup(setup=cleanup) def test_buffer(): eq(vim.current.buffer, vim.windows[0].buffer) vim.command('new') vim.current.window = vim.windows[1] eq(vim.current.buffer, vim.windows[1].buffer) ok(vim.windows[0].buffer != vim.windows[1].buffer) @with_setup(setup=cleanup) def test_cursor(): eq(vim.current.window.cursor, [1, 0]) vim.command('normal ityping\033o some text') eq(vim.current.buffer[:], ['typing', ' some text']) eq(vim.current.window.cursor, [2, 10]) vim.current.window.cursor = [2, 6] vim.command('normal i dumb') eq(vim.current.buffer[:], ['typing', ' some dumb text']) @with_setup(setup=cleanup) def test_height(): vim.command('vsplit') eq(vim.windows[1].height, vim.windows[0].height) vim.current.window = vim.windows[1] vim.command('split') eq(vim.windows[1].height, vim.windows[0].height // 2) vim.windows[1].height = 2 eq(vim.windows[1].height, 2) @with_setup(setup=cleanup) def test_width(): vim.command('split') eq(vim.windows[1].width, vim.windows[0].width) vim.current.window = vim.windows[1] vim.command('vsplit') eq(vim.windows[1].width, vim.windows[0].width // 2) vim.windows[1].width = 2 eq(vim.windows[1].width, 2) @with_setup(setup=cleanup) def test_vars(): vim.current.window.vars['python'] = [1, 2, {'3': 1}] eq(vim.current.window.vars['python'], [1, 2, {'3': 1}]) eq(vim.eval('w:python'), [1, 2, {'3': 1}]) @with_setup(setup=cleanup) def test_options(): vim.current.window.options['colorcolumn'] = '4,3' eq(vim.current.window.options['colorcolumn'], '4,3') # global-local option vim.current.window.options['statusline'] = 'window-status' eq(vim.current.window.options['statusline'], 'window-status') eq(vim.options['statusline'], '') @with_setup(setup=cleanup) def test_position(): height = vim.windows[0].height width = vim.windows[0].width vim.command('split') vim.command('vsplit') eq((vim.windows[0].row, vim.windows[0].col), (0, 0)) vsplit_pos = width / 2 split_pos = height / 2 eq(vim.windows[1].row, 0) ok(vsplit_pos - 1 <= vim.windows[1].col <= vsplit_pos + 1) ok(split_pos - 1 <= vim.windows[2].row <= split_pos + 1) eq(vim.windows[2].col, 0) @with_setup(setup=cleanup) def test_tabpage(): vim.command('tabnew') vim.command('vsplit') eq(vim.windows[0].tabpage, vim.tabpages[0]) eq(vim.windows[1].tabpage, vim.tabpages[1]) eq(vim.windows[2].tabpage, vim.tabpages[1]) @with_setup(setup=cleanup) def test_valid(): vim.command('split') window = vim.windows[1] vim.current.window = window ok(window.valid) vim.command('q') ok(not window.valid) @with_setup(setup=cleanup) def test_number(): curnum = vim.current.window.number vim.command('bot split') eq(vim.current.window.number, curnum + 1) vim.command('bot split') eq(vim.current.window.number, curnum + 2) @with_setup(setup=cleanup) def test_handle(): hnd1 = vim.current.window.handle vim.command('bot split') hnd2 = vim.current.window.handle ok(hnd2 != hnd1) vim.command('bot split') hnd3 = vim.current.window.handle ok(hnd3 != hnd1) ok(hnd3 != hnd2) vim.command('wincmd w') eq(vim.current.window.handle,hnd1) neovim-0.2.0/setup.py0000644000175000017500000000206113200646412014763 0ustar bjornbjorn00000000000000import platform import sys import os from setuptools import setup install_requires = [ 'msgpack-python>=0.4.0', ] extras_require = { 'pyuv': ['pyuv>=1.0.0'], } if os.name == 'nt': install_requires.append('pyuv>=1.0.0') elif sys.version_info < (3, 4): # trollius is just a backport of 3.4 asyncio module install_requires.append('trollius') if platform.python_implementation() != 'PyPy': # pypy already includes an implementation of the greenlet module install_requires.append('greenlet') setup(name='neovim', version='0.2.0', description='Python client to neovim', url='http://github.com/neovim/python-client', download_url='https://github.com/neovim/python-client/archive/0.2.0.tar.gz', author='Thiago de Arruda', author_email='tpadilha84@gmail.com', license='Apache', packages=['neovim', 'neovim.api', 'neovim.msgpack_rpc', 'neovim.msgpack_rpc.event_loop', 'neovim.plugin'], install_requires=install_requires, extras_require=extras_require, zip_safe=False) neovim-0.2.0/PKG-INFO0000644000175000017500000000050313200646543014352 0ustar bjornbjorn00000000000000Metadata-Version: 1.1 Name: neovim Version: 0.2.0 Summary: Python client to neovim Home-page: http://github.com/neovim/python-client Author: Thiago de Arruda Author-email: tpadilha84@gmail.com License: Apache Download-URL: https://github.com/neovim/python-client/archive/0.2.0.tar.gz Description: UNKNOWN Platform: UNKNOWN neovim-0.2.0/neovim.egg-info/0000755000175000017500000000000013200646543016246 5ustar bjornbjorn00000000000000neovim-0.2.0/neovim.egg-info/not-zip-safe0000644000175000017500000000000112362550312020470 0ustar bjornbjorn00000000000000 neovim-0.2.0/neovim.egg-info/dependency_links.txt0000644000175000017500000000000113200646543022314 0ustar bjornbjorn00000000000000 neovim-0.2.0/neovim.egg-info/top_level.txt0000644000175000017500000000000713200646543020775 0ustar bjornbjorn00000000000000neovim neovim-0.2.0/neovim.egg-info/SOURCES.txt0000644000175000017500000000173113200646543020134 0ustar bjornbjorn00000000000000LICENSE MANIFEST.in README.md setup.cfg setup.py neovim/__init__.py neovim/compat.py neovim/util.py neovim/version.py neovim.egg-info/PKG-INFO neovim.egg-info/SOURCES.txt neovim.egg-info/dependency_links.txt neovim.egg-info/not-zip-safe neovim.egg-info/requires.txt neovim.egg-info/top_level.txt neovim/api/__init__.py neovim/api/buffer.py neovim/api/common.py neovim/api/nvim.py neovim/api/tabpage.py neovim/api/window.py neovim/msgpack_rpc/__init__.py neovim/msgpack_rpc/async_session.py neovim/msgpack_rpc/msgpack_stream.py neovim/msgpack_rpc/session.py neovim/msgpack_rpc/event_loop/__init__.py neovim/msgpack_rpc/event_loop/asyncio.py neovim/msgpack_rpc/event_loop/base.py neovim/msgpack_rpc/event_loop/uv.py neovim/plugin/__init__.py neovim/plugin/decorators.py neovim/plugin/host.py neovim/plugin/script_host.py test/test_buffer.py test/test_client_rpc.py test/test_common.py test/test_concurrency.py test/test_events.py test/test_tabpage.py test/test_vim.py test/test_window.pyneovim-0.2.0/neovim.egg-info/PKG-INFO0000644000175000017500000000050313200646543017341 0ustar bjornbjorn00000000000000Metadata-Version: 1.1 Name: neovim Version: 0.2.0 Summary: Python client to neovim Home-page: http://github.com/neovim/python-client Author: Thiago de Arruda Author-email: tpadilha84@gmail.com License: Apache Download-URL: https://github.com/neovim/python-client/archive/0.2.0.tar.gz Description: UNKNOWN Platform: UNKNOWN neovim-0.2.0/neovim.egg-info/requires.txt0000644000175000017500000000006313200646543020645 0ustar bjornbjorn00000000000000msgpack-python>=0.4.0 greenlet [pyuv] pyuv>=1.0.0 neovim-0.2.0/LICENSE0000644000175000017500000002605612405774052014300 0ustar bjornbjorn00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2014 Thiago Arruda Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.