neovim-0.2.0/ 0000755 0001750 0001750 00000000000 13200646543 013257 5 ustar bjorn bjorn 0000000 0000000 neovim-0.2.0/neovim/ 0000755 0001750 0001750 00000000000 13200646543 014554 5 ustar bjorn bjorn 0000000 0000000 neovim-0.2.0/neovim/msgpack_rpc/ 0000755 0001750 0001750 00000000000 13200646543 017045 5 ustar bjorn bjorn 0000000 0000000 neovim-0.2.0/neovim/msgpack_rpc/session.py 0000644 0001750 0001750 00000017703 13200645644 021113 0 ustar bjorn bjorn 0000000 0000000 """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__.py 0000644 0001750 0001750 00000002363 12760330774 021170 0 ustar bjorn bjorn 0000000 0000000 """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/ 0000755 0001750 0001750 00000000000 13200646543 021217 5 ustar bjorn bjorn 0000000 0000000 neovim-0.2.0/neovim/msgpack_rpc/event_loop/__init__.py 0000644 0001750 0001750 00000000741 12415724053 023332 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000007632 13200643523 022226 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000010422 13200643523 023230 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000015115 13010315111 022465 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000011025 12761561140 022276 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000003573 12770005312 022421 0 ustar bjorn bjorn 0000000 0000000 """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/ 0000755 0001750 0001750 00000000000 13200646543 016052 5 ustar bjorn bjorn 0000000 0000000 neovim-0.2.0/neovim/plugin/host.py 0000644 0001750 0001750 00000020326 13200645644 017405 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000020313 13200640620 020752 0 ustar bjorn bjorn 0000000 0000000 """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__.py 0000644 0001750 0001750 00000000477 12770005312 020165 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000011004 13200645644 020566 0 ustar bjorn bjorn 0000000 0000000 """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__.py 0000644 0001750 0001750 00000011637 13200646412 016670 0 ustar bjorn bjorn 0000000 0000000 """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/ 0000755 0001750 0001750 00000000000 13200646543 015325 5 ustar bjorn bjorn 0000000 0000000 neovim-0.2.0/neovim/api/common.py 0000644 0001750 0001750 00000013600 13014014356 017161 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000003665 13010304721 017204 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000001756 13010304721 017277 0 ustar bjorn bjorn 0000000 0000000 """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__.py 0000644 0001750 0001750 00000000573 12770005312 017435 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000035617 13200645644 016665 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000013240 13200643523 017143 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000000163 13200641402 016600 0 ustar bjorn bjorn 0000000 0000000 from .util import Version
VERSION = Version(major=0, minor=1, patch=14, prerelease="dev")
__all__ = ('VERSION',)
neovim-0.2.0/neovim/util.py 0000644 0001750 0001750 00000001667 13111562407 016112 0 ustar bjorn bjorn 0000000 0000000 """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.py 0000644 0001750 0001750 00000002121 12770005312 016377 0 ustar bjorn bjorn 0000000 0000000 """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.in 0000644 0001750 0001750 00000000032 12760330774 015016 0 ustar bjorn bjorn 0000000 0000000 include README.md LICENSE
neovim-0.2.0/README.md 0000644 0001750 0001750 00000016513 13200645644 014545 0 ustar bjorn bjorn 0000000 0000000 ### Python client to [Neovim](https://github.com/neovim/neovim)
[](https://travis-ci.org/neovim/python-client)
[](https://scrutinizer-ci.com/g/neovim/python-client/?branch=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.cfg 0000644 0001750 0001750 00000000115 13200646543 015075 0 ustar bjorn bjorn 0000000 0000000 [flake8]
ignore = D211,E731,F821,D401
[egg_info]
tag_build =
tag_date = 0
neovim-0.2.0/test/ 0000755 0001750 0001750 00000000000 13200646543 014236 5 ustar bjorn bjorn 0000000 0000000 neovim-0.2.0/test/test_client_rpc.py 0000644 0001750 0001750 00000004331 12770005223 017765 0 ustar bjorn bjorn 0000000 0000000 # -*- 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.py 0000644 0001750 0001750 00000002103 13010304721 017232 0 ustar bjorn bjorn 0000000 0000000 import 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.py 0000644 0001750 0001750 00000001361 12760330774 020210 0 ustar bjorn bjorn 0000000 0000000 from 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.py 0000644 0001750 0001750 00000010732 13200643523 016440 0 ustar bjorn bjorn 0000000 0000000 # -*- 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.py 0000644 0001750 0001750 00000003225 12760330774 017163 0 ustar bjorn bjorn 0000000 0000000 # -*- 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.py 0000644 0001750 0001750 00000002521 13003441047 017130 0 ustar bjorn bjorn 0000000 0000000 import 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.py 0000644 0001750 0001750 00000013107 13200643523 017115 0 ustar bjorn bjorn 0000000 0000000 import 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.py 0000644 0001750 0001750 00000006531 13014014356 017155 0 ustar bjorn bjorn 0000000 0000000 import 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.py 0000644 0001750 0001750 00000002061 13200646412 014763 0 ustar bjorn bjorn 0000000 0000000 import 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-INFO 0000644 0001750 0001750 00000000503 13200646543 014352 0 ustar bjorn bjorn 0000000 0000000 Metadata-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/ 0000755 0001750 0001750 00000000000 13200646543 016246 5 ustar bjorn bjorn 0000000 0000000 neovim-0.2.0/neovim.egg-info/not-zip-safe 0000644 0001750 0001750 00000000001 12362550312 020470 0 ustar bjorn bjorn 0000000 0000000
neovim-0.2.0/neovim.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 13200646543 022314 0 ustar bjorn bjorn 0000000 0000000
neovim-0.2.0/neovim.egg-info/top_level.txt 0000644 0001750 0001750 00000000007 13200646543 020775 0 ustar bjorn bjorn 0000000 0000000 neovim
neovim-0.2.0/neovim.egg-info/SOURCES.txt 0000644 0001750 0001750 00000001731 13200646543 020134 0 ustar bjorn bjorn 0000000 0000000 LICENSE
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.py neovim-0.2.0/neovim.egg-info/PKG-INFO 0000644 0001750 0001750 00000000503 13200646543 017341 0 ustar bjorn bjorn 0000000 0000000 Metadata-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.txt 0000644 0001750 0001750 00000000063 13200646543 020645 0 ustar bjorn bjorn 0000000 0000000 msgpack-python>=0.4.0
greenlet
[pyuv]
pyuv>=1.0.0
neovim-0.2.0/LICENSE 0000644 0001750 0001750 00000026056 12405774052 014300 0 ustar bjorn bjorn 0000000 0000000 Apache 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.