Update upstream source from tag 'upstream/0.3.2'
Update to upstream version '0.3.2'
with Debian dir 50daf90f35e1043fc9ce20bc8eee4c5c62054af9
Víctor Cuadrado Juan
4 years ago
0 | 0 | Metadata-Version: 2.1 |
1 | Name: neovim | |
2 | Version: 0.3.0 | |
1 | Name: pynvim | |
2 | Version: 0.3.2 | |
3 | 3 | Summary: Python client to neovim |
4 | 4 | Home-page: http://github.com/neovim/python-client |
5 | 5 | Author: Thiago de Arruda |
6 | 6 | Author-email: tpadilha84@gmail.com |
7 | 7 | License: Apache |
8 | Download-URL: https://github.com/neovim/python-client/archive/0.3.0.tar.gz | |
8 | Download-URL: https://github.com/neovim/python-client/archive/0.3.2.tar.gz | |
9 | 9 | Description: UNKNOWN |
10 | 10 | Platform: UNKNOWN |
11 | Provides-Extra: test | |
11 | 12 | Provides-Extra: pyuv |
12 | Provides-Extra: test |
0 | 0 | ### Pynvim: Python client to [Neovim](https://github.com/neovim/neovim) |
1 | 1 | |
2 | [![Build Status](https://travis-ci.org/neovim/python-client.svg?branch=master)](https://travis-ci.org/neovim/python-client) | |
2 | [![Build Status](https://travis-ci.org/neovim/pynvim.svg?branch=master)](https://travis-ci.org/neovim/pynvim) | |
3 | 3 | [![Documentation Status](https://readthedocs.org/projects/pynvim/badge/?version=latest)](http://pynvim.readthedocs.io/en/latest/?badge=latest) |
4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/neovim/python-client/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/neovim/python-client/?branch=master) | |
5 | [![Code Coverage](https://scrutinizer-ci.com/g/neovim/python-client/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/neovim/python-client/?branch=master) | |
4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/neovim/pynvim/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/neovim/pynvim/?branch=master) | |
5 | [![Code Coverage](https://scrutinizer-ci.com/g/neovim/pynvim/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/neovim/pynvim/?branch=master) | |
6 | 6 | |
7 | 7 | Pynvim implements support for python plugins in Nvim. It also works as a library for |
8 | 8 | connecting to and scripting Nvim processes through its msgpack-rpc API. |
12 | 12 | Supports python 2.7, and 3.4 or later. |
13 | 13 | |
14 | 14 | ```sh |
15 | pip2 install neovim | |
16 | pip3 install neovim | |
15 | pip2 install pynvim | |
16 | pip3 install pynvim | |
17 | 17 | ``` |
18 | 18 | |
19 | 19 | If you only use one of python2 or python3, it is enough to install that |
20 | 20 | version. You can install the package without being root by adding the `--user` |
21 | 21 | flag. |
22 | 22 | |
23 | If you follow Neovim master, make sure to upgrade the python-client when you | |
24 | upgrade neovim: | |
23 | Anytime you upgrade Neovim, make sure to upgrade pynvim as well: | |
25 | 24 | ```sh |
26 | pip2 install --upgrade neovim | |
27 | pip3 install --upgrade neovim | |
25 | pip2 install --upgrade pynvim | |
26 | pip3 install --upgrade pynvim | |
28 | 27 | ``` |
29 | 28 | |
30 | 29 | Alternatively, the master version could be installed by executing the following |
56 | 55 | |
57 | 56 | #### Development |
58 | 57 | |
59 | If you change the code, you need to run | |
60 | ```sh | |
61 | pip2 install . | |
62 | pip3 install . | |
63 | ``` | |
64 | for the changes to have effect. For instructions of testing and troubleshooting, | |
65 | see the [development](http://pynvim.readthedocs.io/en/latest/development.html) documentation. | |
58 | Use (and activate) a local virtualenv. | |
59 | ||
60 | python3 -m venv env36 | |
61 | source env36/bin/activate | |
62 | ||
63 | If you change the code, you must reinstall for the changes to take effect: | |
64 | ||
65 | pip install . | |
66 | ||
67 | Use `pytest` to run the tests. Invoking with `python -m` prepends the current | |
68 | directory to `sys.path` (otherwise `pytest` might find other versions!): | |
69 | ||
70 | python -m pytest | |
71 | ||
72 | For details about testing and troubleshooting, see the | |
73 | [development](http://pynvim.readthedocs.io/en/latest/development.html) | |
74 | documentation. | |
66 | 75 | |
67 | 76 | #### Usage through the python REPL |
68 | 77 | |
79 | 88 | bridge](http://vimdoc.sourceforge.net/htmldoc/if_pyth.html#python-vim)): |
80 | 89 | |
81 | 90 | ```python |
82 | >>> from neovim import attach | |
91 | >>> from pynvim import attach | |
83 | 92 | # Create a python API session attached to unix domain socket created above: |
84 | 93 | >>> nvim = attach('socket', path='/tmp/nvim') |
85 | 94 | # Now do some work. |
97 | 106 | running neovim instance. |
98 | 107 | |
99 | 108 | ```python |
100 | >>> from neovim import attach | |
109 | >>> from pynvim import attach | |
101 | 110 | >>> nvim = attach('child', argv=["/bin/env", "nvim", "--embed"]) |
102 | 111 | ``` |
103 | 112 |
0 | 0 | """Python client for Nvim. |
1 | 1 | |
2 | Client library for talking with Nvim processes via it's msgpack-rpc API. | |
2 | This is a transition package. New projects should instead import pynvim package. | |
3 | 3 | """ |
4 | import logging | |
5 | import os | |
6 | import sys | |
4 | import pynvim | |
5 | from pynvim import * | |
7 | 6 | |
8 | from .api import Nvim | |
9 | from .compat import IS_PYTHON3 | |
10 | from .msgpack_rpc import (ErrorResponse, child_session, socket_session, | |
11 | stdio_session, tcp_session) | |
12 | from .plugin import (Host, autocmd, command, decode, encoding, function, | |
13 | plugin, rpc_export, shutdown_hook) | |
14 | from .util import VERSION, Version | |
15 | ||
16 | ||
17 | __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session', | |
18 | 'start_host', 'autocmd', 'command', 'encoding', 'decode', | |
19 | 'function', 'plugin', 'rpc_export', 'Host', 'Nvim', 'Version', | |
20 | 'VERSION', 'shutdown_hook', 'attach', 'setup_logging', | |
21 | 'ErrorResponse') | |
22 | ||
23 | ||
24 | def start_host(session=None): | |
25 | """Promote the current process into python plugin host for Nvim. | |
26 | ||
27 | Start msgpack-rpc event loop for `session`, listening for Nvim requests | |
28 | and notifications. It registers Nvim commands for loading/unloading | |
29 | python plugins. | |
30 | ||
31 | The sys.stdout and sys.stderr streams are redirected to Nvim through | |
32 | `session`. That means print statements probably won't work as expected | |
33 | while this function doesn't return. | |
34 | ||
35 | This function is normally called at program startup and could have been | |
36 | defined as a separate executable. It is exposed as a library function for | |
37 | testing purposes only. | |
38 | """ | |
39 | plugins = [] | |
40 | for arg in sys.argv: | |
41 | _, ext = os.path.splitext(arg) | |
42 | if ext == '.py': | |
43 | plugins.append(arg) | |
44 | elif os.path.isdir(arg): | |
45 | init = os.path.join(arg, '__init__.py') | |
46 | if os.path.isfile(init): | |
47 | plugins.append(arg) | |
48 | ||
49 | # This is a special case to support the old workaround of | |
50 | # adding an empty .py file to make a package directory | |
51 | # visible, and it should be removed soon. | |
52 | for path in list(plugins): | |
53 | dup = path + ".py" | |
54 | if os.path.isdir(path) and dup in plugins: | |
55 | plugins.remove(dup) | |
56 | ||
57 | # Special case: the legacy scripthost receives a single relative filename | |
58 | # while the rplugin host will receive absolute paths. | |
59 | if plugins == ["script_host.py"]: | |
60 | name = "script" | |
61 | else: | |
62 | name = "rplugin" | |
63 | ||
64 | setup_logging(name) | |
65 | ||
66 | if not session: | |
67 | session = stdio_session() | |
68 | nvim = Nvim.from_session(session) | |
69 | ||
70 | if nvim.version.api_level < 1: | |
71 | sys.stderr.write("This version of the neovim python package " | |
72 | "requires nvim 0.1.6 or later") | |
73 | sys.exit(1) | |
74 | ||
75 | host = Host(nvim) | |
76 | host.start(plugins) | |
77 | ||
78 | ||
79 | def attach(session_type, address=None, port=None, | |
80 | path=None, argv=None, decode=None): | |
81 | """Provide a nicer interface to create python api sessions. | |
82 | ||
83 | Previous machinery to create python api sessions is still there. This only | |
84 | creates a facade function to make things easier for the most usual cases. | |
85 | Thus, instead of: | |
86 | from neovim import socket_session, Nvim | |
87 | session = tcp_session(address=<address>, port=<port>) | |
88 | nvim = Nvim.from_session(session) | |
89 | You can now do: | |
90 | from neovim import attach | |
91 | nvim = attach('tcp', address=<address>, port=<port>) | |
92 | And also: | |
93 | nvim = attach('socket', path=<path>) | |
94 | nvim = attach('child', argv=<argv>) | |
95 | nvim = attach('stdio') | |
96 | ||
97 | When the session is not needed anymore, it is recommended to explicitly | |
98 | close it: | |
99 | nvim.close() | |
100 | It is also possible to use the session as a context mangager: | |
101 | with attach('socket', path=thepath) as nvim: | |
102 | print(nvim.funcs.getpid()) | |
103 | print(nvim.current.line) | |
104 | This will automatically close the session when you're done with it, or | |
105 | when an error occured. | |
106 | ||
107 | ||
108 | """ | |
109 | session = (tcp_session(address, port) if session_type == 'tcp' else | |
110 | socket_session(path) if session_type == 'socket' else | |
111 | stdio_session() if session_type == 'stdio' else | |
112 | child_session(argv) if session_type == 'child' else | |
113 | None) | |
114 | ||
115 | if not session: | |
116 | raise Exception('Unknown session type "%s"' % session_type) | |
117 | ||
118 | if decode is None: | |
119 | decode = IS_PYTHON3 | |
120 | ||
121 | return Nvim.from_session(session).with_decode(decode) | |
122 | ||
123 | ||
124 | def setup_logging(name): | |
125 | """Setup logging according to environment variables.""" | |
126 | logger = logging.getLogger(__name__) | |
127 | if 'NVIM_PYTHON_LOG_FILE' in os.environ: | |
128 | prefix = os.environ['NVIM_PYTHON_LOG_FILE'].strip() | |
129 | major_version = sys.version_info[0] | |
130 | logfile = '{}_py{}_{}'.format(prefix, major_version, name) | |
131 | handler = logging.FileHandler(logfile, 'w', 'utf-8') | |
132 | handler.formatter = logging.Formatter( | |
133 | '%(asctime)s [%(levelname)s @ ' | |
134 | '%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s') | |
135 | logging.root.addHandler(handler) | |
136 | level = logging.INFO | |
137 | if 'NVIM_PYTHON_LOG_LEVEL' in os.environ: | |
138 | lvl = getattr(logging, | |
139 | os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(), | |
140 | level) | |
141 | if isinstance(lvl, int): | |
142 | level = lvl | |
143 | logger.setLevel(level) | |
144 | ||
145 | ||
146 | # Required for python 2.6 | |
147 | class NullHandler(logging.Handler): | |
148 | def emit(self, record): | |
149 | pass | |
150 | ||
151 | ||
152 | if not logging.root.handlers: | |
153 | logging.root.addHandler(NullHandler()) | |
7 | __all__ = pynvim.__all__ |
0 | 0 | """Nvim API subpackage. |
1 | 1 | |
2 | This package implements a higher-level API that wraps msgpack-rpc `Session` | |
3 | instances. | |
2 | This is a transition package. New projects should instead import pynvim.api. | |
4 | 3 | """ |
4 | from pynvim import api | |
5 | from pynvim.api import * | |
5 | 6 | |
6 | from .buffer import Buffer | |
7 | from .common import decode_if_bytes, walk | |
8 | from .nvim import Nvim, NvimError | |
9 | from .tabpage import Tabpage | |
10 | from .window import Window | |
11 | ||
12 | ||
13 | __all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError', | |
14 | 'decode_if_bytes', 'walk') | |
7 | __all__ = api.__all__ |
0 | """API for working with a Nvim Buffer.""" | |
1 | from .common import Remote | |
2 | from ..compat import IS_PYTHON3, check_async | |
3 | ||
4 | ||
5 | __all__ = ('Buffer') | |
6 | ||
7 | ||
8 | if IS_PYTHON3: | |
9 | basestring = str | |
10 | ||
11 | ||
12 | def adjust_index(idx, default=None): | |
13 | """Convert from python indexing convention to nvim indexing convention.""" | |
14 | if idx is None: | |
15 | return default | |
16 | elif idx < 0: | |
17 | return idx - 1 | |
18 | else: | |
19 | return idx | |
20 | ||
21 | ||
22 | class Buffer(Remote): | |
23 | ||
24 | """A remote Nvim buffer.""" | |
25 | ||
26 | _api_prefix = "nvim_buf_" | |
27 | ||
28 | def __len__(self): | |
29 | """Return the number of lines contained in a Buffer.""" | |
30 | return self.request('nvim_buf_line_count') | |
31 | ||
32 | def __getitem__(self, idx): | |
33 | """Get a buffer line or slice by integer index. | |
34 | ||
35 | Indexes may be negative to specify positions from the end of the | |
36 | buffer. For example, -1 is the last line, -2 is the line before that | |
37 | and so on. | |
38 | ||
39 | When retrieving slices, omiting indexes(eg: `buffer[:]`) will bring | |
40 | the whole buffer. | |
41 | """ | |
42 | if not isinstance(idx, slice): | |
43 | i = adjust_index(idx) | |
44 | return self.request('nvim_buf_get_lines', i, i + 1, True)[0] | |
45 | start = adjust_index(idx.start, 0) | |
46 | end = adjust_index(idx.stop, -1) | |
47 | return self.request('nvim_buf_get_lines', start, end, False) | |
48 | ||
49 | def __setitem__(self, idx, item): | |
50 | """Replace a buffer line or slice by integer index. | |
51 | ||
52 | Like with `__getitem__`, indexes may be negative. | |
53 | ||
54 | When replacing slices, omiting indexes(eg: `buffer[:]`) will replace | |
55 | the whole buffer. | |
56 | """ | |
57 | if not isinstance(idx, slice): | |
58 | i = adjust_index(idx) | |
59 | lines = [item] if item is not None else [] | |
60 | return self.request('nvim_buf_set_lines', i, i + 1, True, lines) | |
61 | lines = item if item is not None else [] | |
62 | start = adjust_index(idx.start, 0) | |
63 | end = adjust_index(idx.stop, -1) | |
64 | return self.request('nvim_buf_set_lines', start, end, False, lines) | |
65 | ||
66 | def __iter__(self): | |
67 | """Iterate lines of a buffer. | |
68 | ||
69 | This will retrieve all lines locally before iteration starts. This | |
70 | approach is used because for most cases, the gain is much greater by | |
71 | minimizing the number of API calls by transfering all data needed to | |
72 | work. | |
73 | """ | |
74 | lines = self[:] | |
75 | for line in lines: | |
76 | yield line | |
77 | ||
78 | def __delitem__(self, idx): | |
79 | """Delete line or slice of lines from the buffer. | |
80 | ||
81 | This is the same as __setitem__(idx, []) | |
82 | """ | |
83 | self.__setitem__(idx, None) | |
84 | ||
85 | def append(self, lines, index=-1): | |
86 | """Append a string or list of lines to the buffer.""" | |
87 | if isinstance(lines, (basestring, bytes)): | |
88 | lines = [lines] | |
89 | return self.request('nvim_buf_set_lines', index, index, True, lines) | |
90 | ||
91 | def mark(self, name): | |
92 | """Return (row, col) tuple for a named mark.""" | |
93 | return self.request('nvim_buf_get_mark', name) | |
94 | ||
95 | def range(self, start, end): | |
96 | """Return a `Range` object, which represents part of the Buffer.""" | |
97 | return Range(self, start, end) | |
98 | ||
99 | def add_highlight(self, hl_group, line, col_start=0, | |
100 | col_end=-1, src_id=-1, async_=None, | |
101 | **kwargs): | |
102 | """Add a highlight to the buffer.""" | |
103 | async_ = check_async(async_, kwargs, src_id != 0) | |
104 | return self.request('nvim_buf_add_highlight', src_id, hl_group, | |
105 | line, col_start, col_end, async_=async_) | |
106 | ||
107 | def clear_highlight(self, src_id, line_start=0, line_end=-1, async_=None, | |
108 | **kwargs): | |
109 | """Clear highlights from the buffer.""" | |
110 | async_ = check_async(async_, kwargs, True) | |
111 | self.request('nvim_buf_clear_highlight', src_id, | |
112 | line_start, line_end, async_=async_) | |
113 | ||
114 | def update_highlights(self, src_id, hls, clear_start=0, clear_end=-1, | |
115 | clear=False, async_=True): | |
116 | """Add or update highlights in batch to avoid unnecessary redraws. | |
117 | ||
118 | A `src_id` must have been allocated prior to use of this function. Use | |
119 | for instance `nvim.new_highlight_source()` to get a src_id for your | |
120 | plugin. | |
121 | ||
122 | `hls` should be a list of highlight items. Each item should be a list | |
123 | or tuple on the form `("GroupName", linenr, col_start, col_end)` or | |
124 | `("GroupName", linenr)` to highlight an entire line. | |
125 | ||
126 | By default existing highlights are preserved. Specify a line range with | |
127 | clear_start and clear_end to replace highlights in this range. As a | |
128 | shorthand, use clear=True to clear the entire buffer before adding the | |
129 | new highlights. | |
130 | """ | |
131 | if clear and clear_start is None: | |
132 | clear_start = 0 | |
133 | lua = self._session._get_lua_private() | |
134 | lua.update_highlights(self, src_id, hls, clear_start, clear_end, | |
135 | async_=async_) | |
136 | ||
137 | @property | |
138 | def name(self): | |
139 | """Get the buffer name.""" | |
140 | return self.request('nvim_buf_get_name') | |
141 | ||
142 | @name.setter | |
143 | def name(self, value): | |
144 | """Set the buffer name. BufFilePre/BufFilePost are triggered.""" | |
145 | return self.request('nvim_buf_set_name', value) | |
146 | ||
147 | @property | |
148 | def valid(self): | |
149 | """Return True if the buffer still exists.""" | |
150 | return self.request('nvim_buf_is_valid') | |
151 | ||
152 | @property | |
153 | def number(self): | |
154 | """Get the buffer number.""" | |
155 | return self.handle | |
156 | ||
157 | ||
158 | class Range(object): | |
159 | def __init__(self, buffer, start, end): | |
160 | self._buffer = buffer | |
161 | self.start = start - 1 | |
162 | self.end = end - 1 | |
163 | ||
164 | def __len__(self): | |
165 | return self.end - self.start + 1 | |
166 | ||
167 | def __getitem__(self, idx): | |
168 | if not isinstance(idx, slice): | |
169 | return self._buffer[self._normalize_index(idx)] | |
170 | start = self._normalize_index(idx.start) | |
171 | end = self._normalize_index(idx.stop) | |
172 | if start is None: | |
173 | start = self.start | |
174 | if end is None: | |
175 | end = self.end + 1 | |
176 | return self._buffer[start:end] | |
177 | ||
178 | def __setitem__(self, idx, lines): | |
179 | if not isinstance(idx, slice): | |
180 | self._buffer[self._normalize_index(idx)] = lines | |
181 | return | |
182 | start = self._normalize_index(idx.start) | |
183 | end = self._normalize_index(idx.stop) | |
184 | if start is None: | |
185 | start = self.start | |
186 | if end is None: | |
187 | end = self.end | |
188 | self._buffer[start:end + 1] = lines | |
189 | ||
190 | def __iter__(self): | |
191 | for i in range(self.start, self.end + 1): | |
192 | yield self._buffer[i] | |
193 | ||
194 | def append(self, lines, i=None): | |
195 | i = self._normalize_index(i) | |
196 | if i is None: | |
197 | i = self.end + 1 | |
198 | self._buffer.append(lines, i) | |
199 | ||
200 | def _normalize_index(self, index): | |
201 | if index is None: | |
202 | return None | |
203 | if index < 0: | |
204 | index = self.end | |
205 | else: | |
206 | index += self.start | |
207 | if index > self.end: | |
208 | index = self.end | |
209 | return index |
0 | """Code shared between the API classes.""" | |
1 | import functools | |
2 | ||
3 | from msgpack import unpackb | |
4 | ||
5 | from ..compat import unicode_errors_default | |
6 | ||
7 | ||
8 | class Remote(object): | |
9 | ||
10 | """Base class for Nvim objects(buffer/window/tabpage). | |
11 | ||
12 | Each type of object has it's own specialized class with API wrappers around | |
13 | the msgpack-rpc session. This implements equality which takes the remote | |
14 | object handle into consideration. | |
15 | """ | |
16 | ||
17 | def __init__(self, session, code_data): | |
18 | """Initialize from session and code_data immutable object. | |
19 | ||
20 | The `code_data` contains serialization information required for | |
21 | msgpack-rpc calls. It must be immutable for Buffer equality to work. | |
22 | """ | |
23 | self._session = session | |
24 | self.code_data = code_data | |
25 | self.handle = unpackb(code_data[1]) | |
26 | self.api = RemoteApi(self, self._api_prefix) | |
27 | self.vars = RemoteMap(self, self._api_prefix + 'get_var', | |
28 | self._api_prefix + 'set_var') | |
29 | self.options = RemoteMap(self, self._api_prefix + 'get_option', | |
30 | self._api_prefix + 'set_option') | |
31 | ||
32 | def __repr__(self): | |
33 | """Get text representation of the object.""" | |
34 | return '<%s(handle=%r)>' % ( | |
35 | self.__class__.__name__, | |
36 | self.handle, | |
37 | ) | |
38 | ||
39 | def __eq__(self, other): | |
40 | """Return True if `self` and `other` are the same object.""" | |
41 | return (hasattr(other, 'code_data') and | |
42 | other.code_data == self.code_data) | |
43 | ||
44 | def __hash__(self): | |
45 | """Return hash based on remote object id.""" | |
46 | return self.code_data.__hash__() | |
47 | ||
48 | def request(self, name, *args, **kwargs): | |
49 | """Wrapper for nvim.request.""" | |
50 | return self._session.request(name, self, *args, **kwargs) | |
51 | ||
52 | ||
53 | class RemoteApi(object): | |
54 | ||
55 | """Wrapper to allow api methods to be called like python methods.""" | |
56 | ||
57 | def __init__(self, obj, api_prefix): | |
58 | """Initialize a RemoteApi with object and api prefix.""" | |
59 | self._obj = obj | |
60 | self._api_prefix = api_prefix | |
61 | ||
62 | def __getattr__(self, name): | |
63 | """Return wrapper to named api method.""" | |
64 | return functools.partial(self._obj.request, self._api_prefix + name) | |
65 | ||
66 | ||
67 | class RemoteMap(object): | |
68 | ||
69 | """Represents a string->object map stored in Nvim. | |
70 | ||
71 | This is the dict counterpart to the `RemoteSequence` class, but it is used | |
72 | as a generic way of retrieving values from the various map-like data | |
73 | structures present in Nvim. | |
74 | ||
75 | It is used to provide a dict-like API to vim variables and options. | |
76 | """ | |
77 | ||
78 | def __init__(self, obj, get_method, set_method=None): | |
79 | """Initialize a RemoteMap with session, getter/setter.""" | |
80 | self._get = functools.partial(obj.request, get_method) | |
81 | self._set = None | |
82 | if set_method: | |
83 | self._set = functools.partial(obj.request, set_method) | |
84 | ||
85 | def __getitem__(self, key): | |
86 | """Return a map value by key.""" | |
87 | return self._get(key) | |
88 | ||
89 | def __setitem__(self, key, value): | |
90 | """Set a map value by key(if the setter was provided).""" | |
91 | if not self._set: | |
92 | raise TypeError('This dict is read-only') | |
93 | self._set(key, value) | |
94 | ||
95 | def __delitem__(self, key): | |
96 | """Delete a map value by associating None with the key.""" | |
97 | if not self._set: | |
98 | raise TypeError('This dict is read-only') | |
99 | return self._set(key, None) | |
100 | ||
101 | def __contains__(self, key): | |
102 | """Check if key is present in the map.""" | |
103 | try: | |
104 | self._get(key) | |
105 | return True | |
106 | except Exception: | |
107 | return False | |
108 | ||
109 | def get(self, key, default=None): | |
110 | """Return value for key if present, else a default value.""" | |
111 | try: | |
112 | return self._get(key) | |
113 | except Exception: | |
114 | return default | |
115 | ||
116 | ||
117 | class RemoteSequence(object): | |
118 | ||
119 | """Represents a sequence of objects stored in Nvim. | |
120 | ||
121 | This class is used to wrap msgapck-rpc functions that work on Nvim | |
122 | sequences(of lines, buffers, windows and tabpages) with an API that | |
123 | is similar to the one provided by the python-vim interface. | |
124 | ||
125 | For example, the 'windows' property of the `Nvim` class is a RemoteSequence | |
126 | sequence instance, and the expression `nvim.windows[0]` is translated to | |
127 | session.request('nvim_list_wins')[0]. | |
128 | ||
129 | One important detail about this class is that all methods will fetch the | |
130 | sequence into a list and perform the necessary manipulation | |
131 | locally(iteration, indexing, counting, etc). | |
132 | """ | |
133 | ||
134 | def __init__(self, session, method): | |
135 | """Initialize a RemoteSequence with session, method.""" | |
136 | self._fetch = functools.partial(session.request, method) | |
137 | ||
138 | def __len__(self): | |
139 | """Return the length of the remote sequence.""" | |
140 | return len(self._fetch()) | |
141 | ||
142 | def __getitem__(self, idx): | |
143 | """Return a sequence item by index.""" | |
144 | if not isinstance(idx, slice): | |
145 | return self._fetch()[idx] | |
146 | return self._fetch()[idx.start:idx.stop] | |
147 | ||
148 | def __iter__(self): | |
149 | """Return an iterator for the sequence.""" | |
150 | items = self._fetch() | |
151 | for item in items: | |
152 | yield item | |
153 | ||
154 | def __contains__(self, item): | |
155 | """Check if an item is present in the sequence.""" | |
156 | return item in self._fetch() | |
157 | ||
158 | ||
159 | def _identity(obj, session, method, kind): | |
160 | return obj | |
161 | ||
162 | ||
163 | def decode_if_bytes(obj, mode=True): | |
164 | """Decode obj if it is bytes.""" | |
165 | if mode is True: | |
166 | mode = unicode_errors_default | |
167 | if isinstance(obj, bytes): | |
168 | return obj.decode("utf-8", errors=mode) | |
169 | return obj | |
170 | ||
171 | ||
172 | def walk(fn, obj, *args, **kwargs): | |
173 | """Recursively walk an object graph applying `fn`/`args` to objects.""" | |
174 | if type(obj) in [list, tuple]: | |
175 | return list(walk(fn, o, *args) for o in obj) | |
176 | if type(obj) is dict: | |
177 | return dict((walk(fn, k, *args), walk(fn, v, *args)) for k, v in | |
178 | obj.items()) | |
179 | return fn(obj, *args, **kwargs) |
0 | """Main Nvim interface.""" | |
1 | import os | |
2 | import sys | |
3 | import threading | |
4 | from functools import partial | |
5 | from traceback import format_stack | |
6 | ||
7 | from msgpack import ExtType | |
8 | ||
9 | from .buffer import Buffer | |
10 | from .common import (Remote, RemoteApi, RemoteMap, RemoteSequence, | |
11 | decode_if_bytes, walk) | |
12 | from .tabpage import Tabpage | |
13 | from .window import Window | |
14 | from ..compat import IS_PYTHON3 | |
15 | from ..util import Version, format_exc_skip | |
16 | ||
17 | __all__ = ('Nvim') | |
18 | ||
19 | ||
20 | os_chdir = os.chdir | |
21 | ||
22 | lua_module = """ | |
23 | local a = vim.api | |
24 | local function update_highlights(buf, src_id, hls, clear_first, clear_end) | |
25 | if clear_first ~= nil then | |
26 | a.nvim_buf_clear_highlight(buf, src_id, clear_first, clear_end) | |
27 | end | |
28 | for _,hl in pairs(hls) do | |
29 | local group, line, col_start, col_end = unpack(hl) | |
30 | if col_start == nil then | |
31 | col_start = 0 | |
32 | end | |
33 | if col_end == nil then | |
34 | col_end = -1 | |
35 | end | |
36 | a.nvim_buf_add_highlight(buf, src_id, group, line, col_start, col_end) | |
37 | end | |
38 | end | |
39 | ||
40 | local chid = ... | |
41 | local mod = {update_highlights=update_highlights} | |
42 | _G["_pynvim_"..chid] = mod | |
43 | """ | |
44 | ||
45 | ||
46 | class Nvim(object): | |
47 | ||
48 | """Class that represents a remote Nvim instance. | |
49 | ||
50 | This class is main entry point to Nvim remote API, it is a wrapper | |
51 | around Session instances. | |
52 | ||
53 | The constructor of this class must not be called directly. Instead, the | |
54 | `from_session` class method should be used to create the first instance | |
55 | from a raw `Session` instance. | |
56 | ||
57 | Subsequent instances for the same session can be created by calling the | |
58 | `with_decode` instance method to change the decoding behavior or | |
59 | `SubClass.from_nvim(nvim)` where `SubClass` is a subclass of `Nvim`, which | |
60 | is useful for having multiple `Nvim` objects that behave differently | |
61 | without one affecting the other. | |
62 | ||
63 | When this library is used on python3.4+, asyncio event loop is guaranteed | |
64 | to be used. It is available as the "loop" attribute of this class. Note | |
65 | that asyncio callbacks cannot make blocking requests, which includes | |
66 | accessing state-dependent attributes. They should instead schedule another | |
67 | callback using nvim.async_call, which will not have this restriction. | |
68 | """ | |
69 | ||
70 | @classmethod | |
71 | def from_session(cls, session): | |
72 | """Create a new Nvim instance for a Session instance. | |
73 | ||
74 | This method must be called to create the first Nvim instance, since it | |
75 | queries Nvim metadata for type information and sets a SessionHook for | |
76 | creating specialized objects from Nvim remote handles. | |
77 | """ | |
78 | session.error_wrapper = lambda e: NvimError(e[1]) | |
79 | channel_id, metadata = session.request(b'vim_get_api_info') | |
80 | ||
81 | if IS_PYTHON3: | |
82 | # decode all metadata strings for python3 | |
83 | metadata = walk(decode_if_bytes, metadata) | |
84 | ||
85 | types = { | |
86 | metadata['types']['Buffer']['id']: Buffer, | |
87 | metadata['types']['Window']['id']: Window, | |
88 | metadata['types']['Tabpage']['id']: Tabpage, | |
89 | } | |
90 | ||
91 | return cls(session, channel_id, metadata, types) | |
92 | ||
93 | @classmethod | |
94 | def from_nvim(cls, nvim): | |
95 | """Create a new Nvim instance from an existing instance.""" | |
96 | return cls(nvim._session, nvim.channel_id, nvim.metadata, | |
97 | nvim.types, nvim._decode, nvim._err_cb) | |
98 | ||
99 | def __init__(self, session, channel_id, metadata, types, | |
100 | decode=False, err_cb=None): | |
101 | """Initialize a new Nvim instance. This method is module-private.""" | |
102 | self._session = session | |
103 | self.channel_id = channel_id | |
104 | self.metadata = metadata | |
105 | version = metadata.get("version", {"api_level": 0}) | |
106 | self.version = Version(**version) | |
107 | self.types = types | |
108 | self.api = RemoteApi(self, 'nvim_') | |
109 | self.vars = RemoteMap(self, 'nvim_get_var', 'nvim_set_var') | |
110 | self.vvars = RemoteMap(self, 'nvim_get_vvar', None) | |
111 | self.options = RemoteMap(self, 'nvim_get_option', 'nvim_set_option') | |
112 | self.buffers = Buffers(self) | |
113 | self.windows = RemoteSequence(self, 'nvim_list_wins') | |
114 | self.tabpages = RemoteSequence(self, 'nvim_list_tabpages') | |
115 | self.current = Current(self) | |
116 | self.session = CompatibilitySession(self) | |
117 | self.funcs = Funcs(self) | |
118 | self.lua = LuaFuncs(self) | |
119 | self.error = NvimError | |
120 | self._decode = decode | |
121 | self._err_cb = err_cb | |
122 | ||
123 | # only on python3.4+ we expose asyncio | |
124 | if IS_PYTHON3: | |
125 | self.loop = self._session.loop._loop | |
126 | ||
127 | def _from_nvim(self, obj, decode=None): | |
128 | if decode is None: | |
129 | decode = self._decode | |
130 | if type(obj) is ExtType: | |
131 | cls = self.types[obj.code] | |
132 | return cls(self, (obj.code, obj.data)) | |
133 | if decode: | |
134 | obj = decode_if_bytes(obj, decode) | |
135 | return obj | |
136 | ||
137 | def _to_nvim(self, obj): | |
138 | if isinstance(obj, Remote): | |
139 | return ExtType(*obj.code_data) | |
140 | return obj | |
141 | ||
142 | def _get_lua_private(self): | |
143 | if not getattr(self._session, "_has_lua", False): | |
144 | self.exec_lua(lua_module, self.channel_id) | |
145 | self._session._has_lua = True | |
146 | return getattr(self.lua, "_pynvim_{}".format(self.channel_id)) | |
147 | ||
148 | def request(self, name, *args, **kwargs): | |
149 | r"""Send an API request or notification to nvim. | |
150 | ||
151 | It is rarely needed to call this function directly, as most API | |
152 | functions have python wrapper functions. The `api` object can | |
153 | be also be used to call API functions as methods: | |
154 | ||
155 | vim.api.err_write('ERROR\n', async_=True) | |
156 | vim.current.buffer.api.get_mark('.') | |
157 | ||
158 | is equivalent to | |
159 | ||
160 | vim.request('nvim_err_write', 'ERROR\n', async_=True) | |
161 | vim.request('nvim_buf_get_mark', vim.current.buffer, '.') | |
162 | ||
163 | ||
164 | Normally a blocking request will be sent. If the `async_` flag is | |
165 | present and True, a asynchronous notification is sent instead. This | |
166 | will never block, and the return value or error is ignored. | |
167 | """ | |
168 | if (self._session._loop_thread is not None and | |
169 | threading.current_thread() != self._session._loop_thread): | |
170 | ||
171 | msg = ("request from non-main thread:\n{}\n" | |
172 | .format('\n'.join(format_stack(None, 5)[:-1]))) | |
173 | ||
174 | self.async_call(self._err_cb, msg) | |
175 | raise NvimError("request from non-main thread") | |
176 | ||
177 | decode = kwargs.pop('decode', self._decode) | |
178 | args = walk(self._to_nvim, args) | |
179 | res = self._session.request(name, *args, **kwargs) | |
180 | return walk(self._from_nvim, res, decode=decode) | |
181 | ||
182 | def next_message(self): | |
183 | """Block until a message(request or notification) is available. | |
184 | ||
185 | If any messages were previously enqueued, return the first in queue. | |
186 | If not, run the event loop until one is received. | |
187 | """ | |
188 | msg = self._session.next_message() | |
189 | if msg: | |
190 | return walk(self._from_nvim, msg) | |
191 | ||
192 | def run_loop(self, request_cb, notification_cb, | |
193 | setup_cb=None, err_cb=None): | |
194 | """Run the event loop to receive requests and notifications from Nvim. | |
195 | ||
196 | This should not be called from a plugin running in the host, which | |
197 | already runs the loop and dispatches events to plugins. | |
198 | """ | |
199 | if err_cb is None: | |
200 | err_cb = sys.stderr.write | |
201 | self._err_cb = err_cb | |
202 | ||
203 | def filter_request_cb(name, args): | |
204 | name = self._from_nvim(name) | |
205 | args = walk(self._from_nvim, args) | |
206 | try: | |
207 | result = request_cb(name, args) | |
208 | except Exception: | |
209 | msg = ("error caught in request handler '{} {}'\n{}\n\n" | |
210 | .format(name, args, format_exc_skip(1))) | |
211 | self._err_cb(msg) | |
212 | raise | |
213 | return walk(self._to_nvim, result) | |
214 | ||
215 | def filter_notification_cb(name, args): | |
216 | name = self._from_nvim(name) | |
217 | args = walk(self._from_nvim, args) | |
218 | try: | |
219 | notification_cb(name, args) | |
220 | except Exception: | |
221 | msg = ("error caught in notification handler '{} {}'\n{}\n\n" | |
222 | .format(name, args, format_exc_skip(1))) | |
223 | self._err_cb(msg) | |
224 | raise | |
225 | ||
226 | self._session.run(filter_request_cb, filter_notification_cb, setup_cb) | |
227 | ||
228 | def stop_loop(self): | |
229 | """Stop the event loop being started with `run_loop`.""" | |
230 | self._session.stop() | |
231 | ||
232 | def close(self): | |
233 | """Close the nvim session and release its resources.""" | |
234 | self._session.close() | |
235 | ||
236 | def __enter__(self): | |
237 | """Enter nvim session as a context manager.""" | |
238 | return self | |
239 | ||
240 | def __exit__(self, *exc_info): | |
241 | """Exit nvim session as a context manager. | |
242 | ||
243 | Closes the event loop. | |
244 | """ | |
245 | self.close() | |
246 | ||
247 | def with_decode(self, decode=True): | |
248 | """Initialize a new Nvim instance.""" | |
249 | return Nvim(self._session, self.channel_id, | |
250 | self.metadata, self.types, decode, self._err_cb) | |
251 | ||
252 | def ui_attach(self, width, height, rgb=None, **kwargs): | |
253 | """Register as a remote UI. | |
254 | ||
255 | After this method is called, the client will receive redraw | |
256 | notifications. | |
257 | """ | |
258 | options = kwargs | |
259 | if rgb is not None: | |
260 | options['rgb'] = rgb | |
261 | return self.request('nvim_ui_attach', width, height, options) | |
262 | ||
263 | def ui_detach(self): | |
264 | """Unregister as a remote UI.""" | |
265 | return self.request('nvim_ui_detach') | |
266 | ||
267 | def ui_try_resize(self, width, height): | |
268 | """Notify nvim that the client window has resized. | |
269 | ||
270 | If possible, nvim will send a redraw request to resize. | |
271 | """ | |
272 | return self.request('ui_try_resize', width, height) | |
273 | ||
274 | def subscribe(self, event): | |
275 | """Subscribe to a Nvim event.""" | |
276 | return self.request('nvim_subscribe', event) | |
277 | ||
278 | def unsubscribe(self, event): | |
279 | """Unsubscribe to a Nvim event.""" | |
280 | return self.request('nvim_unsubscribe', event) | |
281 | ||
282 | def command(self, string, **kwargs): | |
283 | """Execute a single ex command.""" | |
284 | return self.request('nvim_command', string, **kwargs) | |
285 | ||
286 | def command_output(self, string): | |
287 | """Execute a single ex command and return the output.""" | |
288 | return self.request('nvim_command_output', string) | |
289 | ||
290 | def eval(self, string, **kwargs): | |
291 | """Evaluate a vimscript expression.""" | |
292 | return self.request('nvim_eval', string, **kwargs) | |
293 | ||
294 | def call(self, name, *args, **kwargs): | |
295 | """Call a vimscript function.""" | |
296 | return self.request('nvim_call_function', name, args, **kwargs) | |
297 | ||
298 | def exec_lua(self, code, *args, **kwargs): | |
299 | """Execute lua code. | |
300 | ||
301 | Additional parameters are available as `...` inside the lua chunk. | |
302 | Only statements are executed. To evaluate an expression, prefix it | |
303 | with `return`: `return my_function(...)` | |
304 | ||
305 | There is a shorthand syntax to call lua functions with arguments: | |
306 | ||
307 | nvim.lua.func(1,2) | |
308 | nvim.lua.mymod.myfunction(data, async_=True) | |
309 | ||
310 | is equivalent to | |
311 | ||
312 | nvim.exec_lua("return func(...)", 1, 2) | |
313 | nvim.exec_lua("mymod.myfunction(...)", data, async_=True) | |
314 | ||
315 | Note that with `async_=True` there is no return value. | |
316 | """ | |
317 | return self.request('nvim_execute_lua', code, args, **kwargs) | |
318 | ||
319 | def strwidth(self, string): | |
320 | """Return the number of display cells `string` occupies. | |
321 | ||
322 | Tab is counted as one cell. | |
323 | """ | |
324 | return self.request('nvim_strwidth', string) | |
325 | ||
326 | def list_runtime_paths(self): | |
327 | """Return a list of paths contained in the 'runtimepath' option.""" | |
328 | return self.request('nvim_list_runtime_paths') | |
329 | ||
330 | def foreach_rtp(self, cb): | |
331 | """Invoke `cb` for each path in 'runtimepath'. | |
332 | ||
333 | Call the given callable for each path in 'runtimepath' until either | |
334 | callable returns something but None, the exception is raised or there | |
335 | are no longer paths. If stopped in case callable returned non-None, | |
336 | vim.foreach_rtp function returns the value returned by callable. | |
337 | """ | |
338 | for path in self.request('nvim_list_runtime_paths'): | |
339 | try: | |
340 | if cb(path) is not None: | |
341 | break | |
342 | except Exception: | |
343 | break | |
344 | ||
345 | def chdir(self, dir_path): | |
346 | """Run os.chdir, then all appropriate vim stuff.""" | |
347 | os_chdir(dir_path) | |
348 | return self.request('nvim_set_current_dir', dir_path) | |
349 | ||
350 | def feedkeys(self, keys, options='', escape_csi=True): | |
351 | """Push `keys` to Nvim user input buffer. | |
352 | ||
353 | Options can be a string with the following character flags: | |
354 | - 'm': Remap keys. This is default. | |
355 | - 'n': Do not remap keys. | |
356 | - 't': Handle keys as if typed; otherwise they are handled as if coming | |
357 | from a mapping. This matters for undo, opening folds, etc. | |
358 | """ | |
359 | return self.request('nvim_feedkeys', keys, options, escape_csi) | |
360 | ||
361 | def input(self, bytes): | |
362 | """Push `bytes` to Nvim low level input buffer. | |
363 | ||
364 | Unlike `feedkeys()`, this uses the lowest level input buffer and the | |
365 | call is not deferred. It returns the number of bytes actually | |
366 | written(which can be less than what was requested if the buffer is | |
367 | full). | |
368 | """ | |
369 | return self.request('nvim_input', bytes) | |
370 | ||
371 | def replace_termcodes(self, string, from_part=False, do_lt=True, | |
372 | special=True): | |
373 | r"""Replace any terminal code strings by byte sequences. | |
374 | ||
375 | The returned sequences are Nvim's internal representation of keys, | |
376 | for example: | |
377 | ||
378 | <esc> -> '\x1b' | |
379 | <cr> -> '\r' | |
380 | <c-l> -> '\x0c' | |
381 | <up> -> '\x80ku' | |
382 | ||
383 | The returned sequences can be used as input to `feedkeys`. | |
384 | """ | |
385 | return self.request('nvim_replace_termcodes', string, | |
386 | from_part, do_lt, special) | |
387 | ||
388 | def out_write(self, msg, **kwargs): | |
389 | """Print `msg` as a normal message.""" | |
390 | return self.request('nvim_out_write', msg, **kwargs) | |
391 | ||
392 | def err_write(self, msg, **kwargs): | |
393 | """Print `msg` as an error message.""" | |
394 | if self._thread_invalid(): | |
395 | # special case: if a non-main thread writes to stderr | |
396 | # i.e. due to an uncaught exception, pass it through | |
397 | # without raising an additional exception. | |
398 | self.async_call(self.err_write, msg, **kwargs) | |
399 | return | |
400 | return self.request('nvim_err_write', msg, **kwargs) | |
401 | ||
402 | def _thread_invalid(self): | |
403 | return (self._session._loop_thread is not None and | |
404 | threading.current_thread() != self._session._loop_thread) | |
405 | ||
406 | def quit(self, quit_command='qa!'): | |
407 | """Send a quit command to Nvim. | |
408 | ||
409 | By default, the quit command is 'qa!' which will make Nvim quit without | |
410 | saving anything. | |
411 | """ | |
412 | try: | |
413 | self.command(quit_command) | |
414 | except IOError: | |
415 | # sending a quit command will raise an IOError because the | |
416 | # connection is closed before a response is received. Safe to | |
417 | # ignore it. | |
418 | pass | |
419 | ||
420 | def new_highlight_source(self): | |
421 | """Return new src_id for use with Buffer.add_highlight.""" | |
422 | return self.current.buffer.add_highlight("", 0, src_id=0) | |
423 | ||
424 | def async_call(self, fn, *args, **kwargs): | |
425 | """Schedule `fn` to be called by the event loop soon. | |
426 | ||
427 | This function is thread-safe, and is the only way code not | |
428 | on the main thread could interact with nvim api objects. | |
429 | ||
430 | This function can also be called in a synchronous | |
431 | event handler, just before it returns, to defer execution | |
432 | that shouldn't block neovim. | |
433 | """ | |
434 | call_point = ''.join(format_stack(None, 5)[:-1]) | |
435 | ||
436 | def handler(): | |
437 | try: | |
438 | fn(*args, **kwargs) | |
439 | except Exception as err: | |
440 | msg = ("error caught while executing async callback:\n" | |
441 | "{!r}\n{}\n \nthe call was requested at\n{}" | |
442 | .format(err, format_exc_skip(1), call_point)) | |
443 | self._err_cb(msg) | |
444 | raise | |
445 | self._session.threadsafe_call(handler) | |
446 | ||
447 | ||
448 | class Buffers(object): | |
449 | ||
450 | """Remote NVim buffers. | |
451 | ||
452 | Currently the interface for interacting with remote NVim buffers is the | |
453 | `nvim_list_bufs` msgpack-rpc function. Most methods fetch the list of | |
454 | buffers from NVim. | |
455 | ||
456 | Conforms to *python-buffers*. | |
457 | """ | |
458 | ||
459 | def __init__(self, nvim): | |
460 | """Initialize a Buffers object with Nvim object `nvim`.""" | |
461 | self._fetch_buffers = nvim.api.list_bufs | |
462 | ||
463 | def __len__(self): | |
464 | """Return the count of buffers.""" | |
465 | return len(self._fetch_buffers()) | |
466 | ||
467 | def __getitem__(self, number): | |
468 | """Return the Buffer object matching buffer number `number`.""" | |
469 | for b in self._fetch_buffers(): | |
470 | if b.number == number: | |
471 | return b | |
472 | raise KeyError(number) | |
473 | ||
474 | def __contains__(self, b): | |
475 | """Return whether Buffer `b` is a known valid buffer.""" | |
476 | return isinstance(b, Buffer) and b.valid | |
477 | ||
478 | def __iter__(self): | |
479 | """Return an iterator over the list of buffers.""" | |
480 | return iter(self._fetch_buffers()) | |
481 | ||
482 | ||
483 | class CompatibilitySession(object): | |
484 | ||
485 | """Helper class for API compatibility.""" | |
486 | ||
487 | def __init__(self, nvim): | |
488 | self.threadsafe_call = nvim.async_call | |
489 | ||
490 | ||
491 | class Current(object): | |
492 | ||
493 | """Helper class for emulating vim.current from python-vim.""" | |
494 | ||
495 | def __init__(self, session): | |
496 | self._session = session | |
497 | self.range = None | |
498 | ||
499 | @property | |
500 | def line(self): | |
501 | return self._session.request('nvim_get_current_line') | |
502 | ||
503 | @line.setter | |
504 | def line(self, line): | |
505 | return self._session.request('nvim_set_current_line', line) | |
506 | ||
507 | @property | |
508 | def buffer(self): | |
509 | return self._session.request('nvim_get_current_buf') | |
510 | ||
511 | @buffer.setter | |
512 | def buffer(self, buffer): | |
513 | return self._session.request('nvim_set_current_buf', buffer) | |
514 | ||
515 | @property | |
516 | def window(self): | |
517 | return self._session.request('nvim_get_current_win') | |
518 | ||
519 | @window.setter | |
520 | def window(self, window): | |
521 | return self._session.request('nvim_set_current_win', window) | |
522 | ||
523 | @property | |
524 | def tabpage(self): | |
525 | return self._session.request('nvim_get_current_tabpage') | |
526 | ||
527 | @tabpage.setter | |
528 | def tabpage(self, tabpage): | |
529 | return self._session.request('nvim_set_current_tabpage', tabpage) | |
530 | ||
531 | ||
532 | class Funcs(object): | |
533 | ||
534 | """Helper class for functional vimscript interface.""" | |
535 | ||
536 | def __init__(self, nvim): | |
537 | self._nvim = nvim | |
538 | ||
539 | def __getattr__(self, name): | |
540 | return partial(self._nvim.call, name) | |
541 | ||
542 | ||
543 | class LuaFuncs(object): | |
544 | ||
545 | """Wrapper to allow lua functions to be called like python methods.""" | |
546 | ||
547 | def __init__(self, nvim, name=""): | |
548 | self._nvim = nvim | |
549 | self.name = name | |
550 | ||
551 | def __getattr__(self, name): | |
552 | """Return wrapper to named api method.""" | |
553 | prefix = self.name + "." if self.name else "" | |
554 | return LuaFuncs(self._nvim, prefix + name) | |
555 | ||
556 | def __call__(self, *args, **kwargs): | |
557 | # first new function after keyword rename, be a bit noisy | |
558 | if 'async' in kwargs: | |
559 | raise ValueError('"async" argument is not allowed. ' | |
560 | 'Use "async_" instead.') | |
561 | async_ = kwargs.get('async_', False) | |
562 | pattern = "return {}(...)" if not async_ else "{}(...)" | |
563 | code = pattern.format(self.name) | |
564 | return self._nvim.exec_lua(code, *args, **kwargs) | |
565 | ||
566 | ||
567 | class NvimError(Exception): | |
568 | pass |
0 | """API for working with Nvim tabpages.""" | |
1 | from .common import Remote, RemoteSequence | |
2 | ||
3 | ||
4 | __all__ = ('Tabpage') | |
5 | ||
6 | ||
7 | class Tabpage(Remote): | |
8 | """A remote Nvim tabpage.""" | |
9 | ||
10 | _api_prefix = "nvim_tabpage_" | |
11 | ||
12 | def __init__(self, *args): | |
13 | """Initialize from session and code_data immutable object. | |
14 | ||
15 | The `code_data` contains serialization information required for | |
16 | msgpack-rpc calls. It must be immutable for Buffer equality to work. | |
17 | """ | |
18 | super(Tabpage, self).__init__(*args) | |
19 | self.windows = RemoteSequence(self, 'nvim_tabpage_list_wins') | |
20 | ||
21 | @property | |
22 | def window(self): | |
23 | """Get the `Window` currently focused on the tabpage.""" | |
24 | return self.request('nvim_tabpage_get_win') | |
25 | ||
26 | @property | |
27 | def valid(self): | |
28 | """Return True if the tabpage still exists.""" | |
29 | return self.request('nvim_tabpage_is_valid') | |
30 | ||
31 | @property | |
32 | def number(self): | |
33 | """Get the tabpage number.""" | |
34 | return self.request('nvim_tabpage_get_number') |
0 | """API for working with Nvim windows.""" | |
1 | from .common import Remote | |
2 | ||
3 | ||
4 | __all__ = ('Window') | |
5 | ||
6 | ||
7 | class Window(Remote): | |
8 | ||
9 | """A remote Nvim window.""" | |
10 | ||
11 | _api_prefix = "nvim_win_" | |
12 | ||
13 | @property | |
14 | def buffer(self): | |
15 | """Get the `Buffer` currently being displayed by the window.""" | |
16 | return self.request('nvim_win_get_buf') | |
17 | ||
18 | @property | |
19 | def cursor(self): | |
20 | """Get the (row, col) tuple with the current cursor position.""" | |
21 | return self.request('nvim_win_get_cursor') | |
22 | ||
23 | @cursor.setter | |
24 | def cursor(self, pos): | |
25 | """Set the (row, col) tuple as the new cursor position.""" | |
26 | return self.request('nvim_win_set_cursor', pos) | |
27 | ||
28 | @property | |
29 | def height(self): | |
30 | """Get the window height in rows.""" | |
31 | return self.request('nvim_win_get_height') | |
32 | ||
33 | @height.setter | |
34 | def height(self, height): | |
35 | """Set the window height in rows.""" | |
36 | return self.request('nvim_win_set_height', height) | |
37 | ||
38 | @property | |
39 | def width(self): | |
40 | """Get the window width in rows.""" | |
41 | return self.request('nvim_win_get_width') | |
42 | ||
43 | @width.setter | |
44 | def width(self, width): | |
45 | """Set the window height in rows.""" | |
46 | return self.request('nvim_win_set_width', width) | |
47 | ||
48 | @property | |
49 | def row(self): | |
50 | """0-indexed, on-screen window position(row) in display cells.""" | |
51 | return self.request('nvim_win_get_position')[0] | |
52 | ||
53 | @property | |
54 | def col(self): | |
55 | """0-indexed, on-screen window position(col) in display cells.""" | |
56 | return self.request('nvim_win_get_position')[1] | |
57 | ||
58 | @property | |
59 | def tabpage(self): | |
60 | """Get the `Tabpage` that contains the window.""" | |
61 | return self.request('nvim_win_get_tabpage') | |
62 | ||
63 | @property | |
64 | def valid(self): | |
65 | """Return True if the window still exists.""" | |
66 | return self.request('nvim_win_is_valid') | |
67 | ||
68 | @property | |
69 | def number(self): | |
70 | """Get the window number.""" | |
71 | return self.request('nvim_win_get_number') |
0 | """Code for supporting compatibility across python versions.""" | |
1 | ||
2 | import sys | |
3 | import warnings | |
4 | from imp import find_module as original_find_module | |
5 | ||
6 | ||
7 | IS_PYTHON3 = sys.version_info >= (3, 0) | |
8 | ||
9 | ||
10 | if IS_PYTHON3: | |
11 | def find_module(fullname, path): | |
12 | """Compatibility wrapper for imp.find_module. | |
13 | ||
14 | Automatically decodes arguments of find_module, in Python3 | |
15 | they must be Unicode | |
16 | """ | |
17 | if isinstance(fullname, bytes): | |
18 | fullname = fullname.decode() | |
19 | if isinstance(path, bytes): | |
20 | path = path.decode() | |
21 | elif isinstance(path, list): | |
22 | newpath = [] | |
23 | for element in path: | |
24 | if isinstance(element, bytes): | |
25 | newpath.append(element.decode()) | |
26 | else: | |
27 | newpath.append(element) | |
28 | path = newpath | |
29 | return original_find_module(fullname, path) | |
30 | ||
31 | # There is no 'long' type in Python3 just int | |
32 | long = int | |
33 | unicode_errors_default = 'surrogateescape' | |
34 | else: | |
35 | find_module = original_find_module | |
36 | unicode_errors_default = 'strict' | |
37 | ||
38 | NUM_TYPES = (int, long, float) | |
39 | ||
40 | ||
41 | def check_async(async_, kwargs, default): | |
42 | """Return a value of 'async' in kwargs or default when async_ is None. | |
43 | ||
44 | This helper function exists for backward compatibility (See #274). | |
45 | It shows a warning message when 'async' in kwargs is used to note users. | |
46 | """ | |
47 | if async_ is not None: | |
48 | return async_ | |
49 | elif 'async' in kwargs: | |
50 | warnings.warn( | |
51 | '"async" attribute is deprecated. Use "async_" instead.', | |
52 | DeprecationWarning, | |
53 | ) | |
54 | return kwargs.pop('async') | |
55 | else: | |
56 | return default |
0 | """Msgpack-rpc subpackage. | |
1 | ||
2 | This package implements a msgpack-rpc client. While it was designed for | |
3 | handling some Nvim particularities(server->client requests for example), the | |
4 | code here should work with other msgpack-rpc servers. | |
5 | """ | |
6 | from .async_session import AsyncSession | |
7 | from .event_loop import EventLoop | |
8 | from .msgpack_stream import MsgpackStream | |
9 | from .session import ErrorResponse, Session | |
10 | ||
11 | ||
12 | __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session', | |
13 | 'ErrorResponse') | |
14 | ||
15 | ||
16 | def session(transport_type='stdio', *args, **kwargs): | |
17 | loop = EventLoop(transport_type, *args, **kwargs) | |
18 | msgpack_stream = MsgpackStream(loop) | |
19 | async_session = AsyncSession(msgpack_stream) | |
20 | session = Session(async_session) | |
21 | return session | |
22 | ||
23 | ||
24 | def tcp_session(address, port=7450): | |
25 | """Create a msgpack-rpc session from a tcp address/port.""" | |
26 | return session('tcp', address, port) | |
27 | ||
28 | ||
29 | def socket_session(path): | |
30 | """Create a msgpack-rpc session from a unix domain socket.""" | |
31 | return session('socket', path) | |
32 | ||
33 | ||
34 | def stdio_session(): | |
35 | """Create a msgpack-rpc session from stdin/stdout.""" | |
36 | return session('stdio') | |
37 | ||
38 | ||
39 | def child_session(argv): | |
40 | """Create a msgpack-rpc session from a new Nvim instance.""" | |
41 | return session('child', argv) |
0 | """Asynchronous msgpack-rpc handling in the event loop pipeline.""" | |
1 | import logging | |
2 | from traceback import format_exc | |
3 | ||
4 | ||
5 | logger = logging.getLogger(__name__) | |
6 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
7 | ||
8 | ||
9 | class AsyncSession(object): | |
10 | ||
11 | """Asynchronous msgpack-rpc layer that wraps a msgpack stream. | |
12 | ||
13 | This wraps the msgpack stream interface for reading/writing msgpack | |
14 | documents and exposes an interface for sending and receiving msgpack-rpc | |
15 | requests and notifications. | |
16 | """ | |
17 | ||
18 | def __init__(self, msgpack_stream): | |
19 | """Wrap `msgpack_stream` on a msgpack-rpc interface.""" | |
20 | self._msgpack_stream = msgpack_stream | |
21 | self._next_request_id = 1 | |
22 | self._pending_requests = {} | |
23 | self._request_cb = self._notification_cb = None | |
24 | self._handlers = { | |
25 | 0: self._on_request, | |
26 | 1: self._on_response, | |
27 | 2: self._on_notification | |
28 | } | |
29 | self.loop = msgpack_stream.loop | |
30 | ||
31 | def threadsafe_call(self, fn): | |
32 | """Wrapper around `MsgpackStream.threadsafe_call`.""" | |
33 | self._msgpack_stream.threadsafe_call(fn) | |
34 | ||
35 | def request(self, method, args, response_cb): | |
36 | """Send a msgpack-rpc request to Nvim. | |
37 | ||
38 | A msgpack-rpc with method `method` and argument `args` is sent to | |
39 | Nvim. The `response_cb` function is called with when the response | |
40 | is available. | |
41 | """ | |
42 | request_id = self._next_request_id | |
43 | self._next_request_id = request_id + 1 | |
44 | self._msgpack_stream.send([0, request_id, method, args]) | |
45 | self._pending_requests[request_id] = response_cb | |
46 | ||
47 | def notify(self, method, args): | |
48 | """Send a msgpack-rpc notification to Nvim. | |
49 | ||
50 | A msgpack-rpc with method `method` and argument `args` is sent to | |
51 | Nvim. This will have the same effect as a request, but no response | |
52 | will be recieved | |
53 | """ | |
54 | self._msgpack_stream.send([2, method, args]) | |
55 | ||
56 | def run(self, request_cb, notification_cb): | |
57 | """Run the event loop to receive requests and notifications from Nvim. | |
58 | ||
59 | While the event loop is running, `request_cb` and `_notification_cb` | |
60 | will be called whenever requests or notifications are respectively | |
61 | available. | |
62 | """ | |
63 | self._request_cb = request_cb | |
64 | self._notification_cb = notification_cb | |
65 | self._msgpack_stream.run(self._on_message) | |
66 | self._request_cb = None | |
67 | self._notification_cb = None | |
68 | ||
69 | def stop(self): | |
70 | """Stop the event loop.""" | |
71 | self._msgpack_stream.stop() | |
72 | ||
73 | def close(self): | |
74 | """Close the event loop.""" | |
75 | self._msgpack_stream.close() | |
76 | ||
77 | def _on_message(self, msg): | |
78 | try: | |
79 | self._handlers.get(msg[0], self._on_invalid_message)(msg) | |
80 | except Exception: | |
81 | err_str = format_exc(5) | |
82 | warn(err_str) | |
83 | self._msgpack_stream.send([1, 0, err_str, None]) | |
84 | ||
85 | def _on_request(self, msg): | |
86 | # request | |
87 | # - msg[1]: id | |
88 | # - msg[2]: method name | |
89 | # - msg[3]: arguments | |
90 | debug('received request: %s, %s', msg[2], msg[3]) | |
91 | self._request_cb(msg[2], msg[3], Response(self._msgpack_stream, | |
92 | msg[1])) | |
93 | ||
94 | def _on_response(self, msg): | |
95 | # response to a previous request: | |
96 | # - msg[1]: the id | |
97 | # - msg[2]: error(if any) | |
98 | # - msg[3]: result(if not errored) | |
99 | debug('received response: %s, %s', msg[2], msg[3]) | |
100 | self._pending_requests.pop(msg[1])(msg[2], msg[3]) | |
101 | ||
102 | def _on_notification(self, msg): | |
103 | # notification/event | |
104 | # - msg[1]: event name | |
105 | # - msg[2]: arguments | |
106 | debug('received notification: %s, %s', msg[1], msg[2]) | |
107 | self._notification_cb(msg[1], msg[2]) | |
108 | ||
109 | def _on_invalid_message(self, msg): | |
110 | error = 'Received invalid message %s' % msg | |
111 | warn(error) | |
112 | self._msgpack_stream.send([1, 0, error, None]) | |
113 | ||
114 | ||
115 | class Response(object): | |
116 | ||
117 | """Response to a msgpack-rpc request that came from Nvim. | |
118 | ||
119 | When Nvim sends a msgpack-rpc request, an instance of this class is | |
120 | created for remembering state required to send a response. | |
121 | """ | |
122 | ||
123 | def __init__(self, msgpack_stream, request_id): | |
124 | """Initialize the Response instance.""" | |
125 | self._msgpack_stream = msgpack_stream | |
126 | self._request_id = request_id | |
127 | ||
128 | def send(self, value, error=False): | |
129 | """Send the response. | |
130 | ||
131 | If `error` is True, it will be sent as an error. | |
132 | """ | |
133 | if error: | |
134 | resp = [1, self._request_id, value, None] | |
135 | else: | |
136 | resp = [1, self._request_id, None, value] | |
137 | debug('sending response to request %d: %s', self._request_id, resp) | |
138 | self._msgpack_stream.send(resp) |
0 | """Event loop abstraction subpackage. | |
1 | ||
2 | Tries to use pyuv as a backend, falling back to the asyncio implementation. | |
3 | """ | |
4 | ||
5 | from ...compat import IS_PYTHON3 | |
6 | ||
7 | # on python3 we only support asyncio, as we expose it to plugins | |
8 | if IS_PYTHON3: | |
9 | from .asyncio import AsyncioEventLoop | |
10 | EventLoop = AsyncioEventLoop | |
11 | else: | |
12 | try: | |
13 | # libuv is fully implemented in C, use it when available | |
14 | from .uv import UvEventLoop | |
15 | EventLoop = UvEventLoop | |
16 | except ImportError: | |
17 | # asyncio(trollius on python 2) is pure python and should be more | |
18 | # portable across python implementations | |
19 | from .asyncio import AsyncioEventLoop | |
20 | EventLoop = AsyncioEventLoop | |
21 | ||
22 | ||
23 | __all__ = ('EventLoop') |
0 | """Event loop implementation that uses the `asyncio` standard module. | |
1 | ||
2 | The `asyncio` module was added to python standard library on 3.4, and it | |
3 | provides a pure python implementation of an event loop library. It is used | |
4 | as a fallback in case pyuv is not available(on python implementations other | |
5 | than CPython). | |
6 | ||
7 | Earlier python versions are supported through the `trollius` package, which | |
8 | is a backport of `asyncio` that works on Python 2.6+. | |
9 | """ | |
10 | from __future__ import absolute_import | |
11 | ||
12 | import logging | |
13 | import os | |
14 | import sys | |
15 | from collections import deque | |
16 | ||
17 | try: | |
18 | # For python 3.4+, use the standard library module | |
19 | import asyncio | |
20 | except (ImportError, SyntaxError): | |
21 | # Fallback to trollius | |
22 | import trollius as asyncio | |
23 | ||
24 | from .base import BaseEventLoop | |
25 | ||
26 | logger = logging.getLogger(__name__) | |
27 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
28 | ||
29 | loop_cls = asyncio.SelectorEventLoop | |
30 | if os.name == 'nt': | |
31 | from asyncio.windows_utils import PipeHandle | |
32 | import msvcrt | |
33 | ||
34 | # On windows use ProactorEventLoop which support pipes and is backed by the | |
35 | # more powerful IOCP facility | |
36 | # NOTE: we override in the stdio case, because it doesn't work. | |
37 | loop_cls = asyncio.ProactorEventLoop | |
38 | ||
39 | ||
40 | class AsyncioEventLoop(BaseEventLoop, asyncio.Protocol, | |
41 | asyncio.SubprocessProtocol): | |
42 | ||
43 | """`BaseEventLoop` subclass that uses `asyncio` as a backend.""" | |
44 | ||
45 | def connection_made(self, transport): | |
46 | """Used to signal `asyncio.Protocol` of a successful connection.""" | |
47 | self._transport = transport | |
48 | self._raw_transport = transport | |
49 | if isinstance(transport, asyncio.SubprocessTransport): | |
50 | self._transport = transport.get_pipe_transport(0) | |
51 | ||
52 | def connection_lost(self, exc): | |
53 | """Used to signal `asyncio.Protocol` of a lost connection.""" | |
54 | self._on_error(exc.args[0] if exc else 'EOF') | |
55 | ||
56 | def data_received(self, data): | |
57 | """Used to signal `asyncio.Protocol` of incoming data.""" | |
58 | if self._on_data: | |
59 | self._on_data(data) | |
60 | return | |
61 | self._queued_data.append(data) | |
62 | ||
63 | def pipe_connection_lost(self, fd, exc): | |
64 | """Used to signal `asyncio.SubprocessProtocol` of a lost connection.""" | |
65 | self._on_error(exc.args[0] if exc else 'EOF') | |
66 | ||
67 | def pipe_data_received(self, fd, data): | |
68 | """Used to signal `asyncio.SubprocessProtocol` of incoming data.""" | |
69 | if fd == 2: # stderr fd number | |
70 | self._on_stderr(data) | |
71 | elif self._on_data: | |
72 | self._on_data(data) | |
73 | else: | |
74 | self._queued_data.append(data) | |
75 | ||
76 | def process_exited(self): | |
77 | """Used to signal `asyncio.SubprocessProtocol` when the child exits.""" | |
78 | self._on_error('EOF') | |
79 | ||
80 | def _init(self): | |
81 | self._loop = loop_cls() | |
82 | self._queued_data = deque() | |
83 | self._fact = lambda: self | |
84 | self._raw_transport = None | |
85 | ||
86 | def _connect_tcp(self, address, port): | |
87 | coroutine = self._loop.create_connection(self._fact, address, port) | |
88 | self._loop.run_until_complete(coroutine) | |
89 | ||
90 | def _connect_socket(self, path): | |
91 | if os.name == 'nt': | |
92 | coroutine = self._loop.create_pipe_connection(self._fact, path) | |
93 | else: | |
94 | coroutine = self._loop.create_unix_connection(self._fact, path) | |
95 | self._loop.run_until_complete(coroutine) | |
96 | ||
97 | def _connect_stdio(self): | |
98 | if os.name == 'nt': | |
99 | pipe = PipeHandle(msvcrt.get_osfhandle(sys.stdin.fileno())) | |
100 | else: | |
101 | pipe = sys.stdin | |
102 | coroutine = self._loop.connect_read_pipe(self._fact, pipe) | |
103 | self._loop.run_until_complete(coroutine) | |
104 | debug("native stdin connection successful") | |
105 | ||
106 | if os.name == 'nt': | |
107 | pipe = PipeHandle(msvcrt.get_osfhandle(sys.stdout.fileno())) | |
108 | else: | |
109 | pipe = sys.stdout | |
110 | coroutine = self._loop.connect_write_pipe(self._fact, pipe) | |
111 | self._loop.run_until_complete(coroutine) | |
112 | debug("native stdout connection successful") | |
113 | ||
114 | def _connect_child(self, argv): | |
115 | if os.name != 'nt': | |
116 | self._child_watcher = asyncio.get_child_watcher() | |
117 | self._child_watcher.attach_loop(self._loop) | |
118 | coroutine = self._loop.subprocess_exec(self._fact, *argv) | |
119 | self._loop.run_until_complete(coroutine) | |
120 | ||
121 | def _start_reading(self): | |
122 | pass | |
123 | ||
124 | def _send(self, data): | |
125 | self._transport.write(data) | |
126 | ||
127 | def _run(self): | |
128 | while self._queued_data: | |
129 | self._on_data(self._queued_data.popleft()) | |
130 | self._loop.run_forever() | |
131 | ||
132 | def _stop(self): | |
133 | self._loop.stop() | |
134 | ||
135 | def _close(self): | |
136 | if self._raw_transport is not None: | |
137 | self._raw_transport.close() | |
138 | self._loop.close() | |
139 | ||
140 | def _threadsafe_call(self, fn): | |
141 | self._loop.call_soon_threadsafe(fn) | |
142 | ||
143 | def _setup_signals(self, signals): | |
144 | if os.name == 'nt': | |
145 | # add_signal_handler is not supported in win32 | |
146 | self._signals = [] | |
147 | return | |
148 | ||
149 | self._signals = list(signals) | |
150 | for signum in self._signals: | |
151 | self._loop.add_signal_handler(signum, self._on_signal, signum) | |
152 | ||
153 | def _teardown_signals(self): | |
154 | for signum in self._signals: | |
155 | self._loop.remove_signal_handler(signum) |
0 | """Common code for event loop implementations.""" | |
1 | import logging | |
2 | import signal | |
3 | import threading | |
4 | ||
5 | ||
6 | logger = logging.getLogger(__name__) | |
7 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
8 | ||
9 | ||
10 | # When signals are restored, the event loop library may reset SIGINT to SIG_DFL | |
11 | # which exits the program. To be able to restore the python interpreter to it's | |
12 | # default state, we keep a reference to the default handler | |
13 | default_int_handler = signal.getsignal(signal.SIGINT) | |
14 | main_thread = threading.current_thread() | |
15 | ||
16 | ||
17 | class BaseEventLoop(object): | |
18 | ||
19 | """Abstract base class for all event loops. | |
20 | ||
21 | Event loops act as the bottom layer for Nvim sessions created by this | |
22 | library. They hide system/transport details behind a simple interface for | |
23 | reading/writing bytes to the connected Nvim instance. | |
24 | ||
25 | This class exposes public methods for interacting with the underlying | |
26 | event loop and delegates implementation-specific work to the following | |
27 | methods, which subclasses are expected to implement: | |
28 | ||
29 | - `_init()`: Implementation-specific initialization | |
30 | - `_connect_tcp(address, port)`: connect to Nvim using tcp/ip | |
31 | - `_connect_socket(path)`: Same as tcp, but use a UNIX domain socket or | |
32 | named pipe. | |
33 | - `_connect_stdio()`: Use stdin/stdout as the connection to Nvim | |
34 | - `_connect_child(argv)`: Use the argument vector `argv` to spawn an | |
35 | embedded Nvim that has it's stdin/stdout connected to the event loop. | |
36 | - `_start_reading()`: Called after any of _connect_* methods. Can be used | |
37 | to perform any post-connection setup or validation. | |
38 | - `_send(data)`: Send `data`(byte array) to Nvim. The data is only | |
39 | - `_run()`: Runs the event loop until stopped or the connection is closed. | |
40 | calling the following methods when some event happens: | |
41 | actually sent when the event loop is running. | |
42 | - `_on_data(data)`: When Nvim sends some data. | |
43 | - `_on_signal(signum)`: When a signal is received. | |
44 | - `_on_error(message)`: When a non-recoverable error occurs(eg: | |
45 | connection lost) | |
46 | - `_stop()`: Stop the event loop | |
47 | - `_interrupt(data)`: Like `stop()`, but may be called from other threads | |
48 | this. | |
49 | - `_setup_signals(signals)`: Add implementation-specific listeners for | |
50 | for `signals`, which is a list of OS-specific signal numbers. | |
51 | - `_teardown_signals()`: Removes signal listeners set by `_setup_signals` | |
52 | """ | |
53 | ||
54 | def __init__(self, transport_type, *args): | |
55 | """Initialize and connect the event loop instance. | |
56 | ||
57 | The only arguments are the transport type and transport-specific | |
58 | configuration, like this: | |
59 | ||
60 | >>> BaseEventLoop('tcp', '127.0.0.1', 7450) | |
61 | Traceback (most recent call last): | |
62 | ... | |
63 | AttributeError: 'BaseEventLoop' object has no attribute '_init' | |
64 | >>> BaseEventLoop('socket', '/tmp/nvim-socket') | |
65 | Traceback (most recent call last): | |
66 | ... | |
67 | AttributeError: 'BaseEventLoop' object has no attribute '_init' | |
68 | >>> BaseEventLoop('stdio') | |
69 | Traceback (most recent call last): | |
70 | ... | |
71 | AttributeError: 'BaseEventLoop' object has no attribute '_init' | |
72 | >>> BaseEventLoop('child', ['nvim', '--embed', '-u', 'NONE']) | |
73 | Traceback (most recent call last): | |
74 | ... | |
75 | AttributeError: 'BaseEventLoop' object has no attribute '_init' | |
76 | ||
77 | This calls the implementation-specific initialization | |
78 | `_init`, one of the `_connect_*` methods(based on `transport_type`) | |
79 | and `_start_reading()` | |
80 | """ | |
81 | self._transport_type = transport_type | |
82 | self._signames = dict((k, v) for v, k in signal.__dict__.items() | |
83 | if v.startswith('SIG')) | |
84 | self._on_data = None | |
85 | self._error = None | |
86 | self._init() | |
87 | try: | |
88 | getattr(self, '_connect_{}'.format(transport_type))(*args) | |
89 | except Exception as e: | |
90 | self.close() | |
91 | raise e | |
92 | self._start_reading() | |
93 | ||
94 | def connect_tcp(self, address, port): | |
95 | """Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`.""" | |
96 | info('Connecting to TCP address: %s:%d', address, port) | |
97 | self._connect_tcp(address, port) | |
98 | ||
99 | def connect_socket(self, path): | |
100 | """Connect to socket at `path`. Delegated to `_connect_socket`.""" | |
101 | info('Connecting to %s', path) | |
102 | self._connect_socket(path) | |
103 | ||
104 | def connect_stdio(self): | |
105 | """Connect using stdin/stdout. Delegated to `_connect_stdio`.""" | |
106 | info('Preparing stdin/stdout for streaming data') | |
107 | self._connect_stdio() | |
108 | ||
109 | def connect_child(self, argv): | |
110 | """Connect a new Nvim instance. Delegated to `_connect_child`.""" | |
111 | info('Spawning a new nvim instance') | |
112 | self._connect_child(argv) | |
113 | ||
114 | def send(self, data): | |
115 | """Queue `data` for sending to Nvim.""" | |
116 | debug("Sending '%s'", data) | |
117 | self._send(data) | |
118 | ||
119 | def threadsafe_call(self, fn): | |
120 | """Call a function in the event loop thread. | |
121 | ||
122 | This is the only safe way to interact with a session from other | |
123 | threads. | |
124 | """ | |
125 | self._threadsafe_call(fn) | |
126 | ||
127 | def run(self, data_cb): | |
128 | """Run the event loop.""" | |
129 | if self._error: | |
130 | err = self._error | |
131 | if isinstance(self._error, KeyboardInterrupt): | |
132 | # KeyboardInterrupt is not destructive(it may be used in | |
133 | # the REPL). | |
134 | # After throwing KeyboardInterrupt, cleanup the _error field | |
135 | # so the loop may be started again | |
136 | self._error = None | |
137 | raise err | |
138 | self._on_data = data_cb | |
139 | if threading.current_thread() == main_thread: | |
140 | self._setup_signals([signal.SIGINT, signal.SIGTERM]) | |
141 | debug('Entering event loop') | |
142 | self._run() | |
143 | debug('Exited event loop') | |
144 | if threading.current_thread() == main_thread: | |
145 | self._teardown_signals() | |
146 | signal.signal(signal.SIGINT, default_int_handler) | |
147 | self._on_data = None | |
148 | ||
149 | def stop(self): | |
150 | """Stop the event loop.""" | |
151 | self._stop() | |
152 | debug('Stopped event loop') | |
153 | ||
154 | def close(self): | |
155 | """Stop the event loop.""" | |
156 | self._close() | |
157 | debug('Closed event loop') | |
158 | ||
159 | def _on_signal(self, signum): | |
160 | msg = 'Received {}'.format(self._signames[signum]) | |
161 | debug(msg) | |
162 | if signum == signal.SIGINT and self._transport_type == 'stdio': | |
163 | # When the transport is stdio, we are probably running as a Nvim | |
164 | # child process. In that case, we don't want to be killed by | |
165 | # ctrl+C | |
166 | return | |
167 | cls = Exception | |
168 | if signum == signal.SIGINT: | |
169 | cls = KeyboardInterrupt | |
170 | self._error = cls(msg) | |
171 | self.stop() | |
172 | ||
173 | def _on_error(self, error): | |
174 | debug(error) | |
175 | self._error = IOError(error) | |
176 | self.stop() | |
177 | ||
178 | def _on_interrupt(self): | |
179 | self.stop() |
0 | """Event loop implementation that uses pyuv(libuv-python bindings).""" | |
1 | import sys | |
2 | from collections import deque | |
3 | ||
4 | import pyuv | |
5 | ||
6 | from .base import BaseEventLoop | |
7 | ||
8 | ||
9 | class UvEventLoop(BaseEventLoop): | |
10 | ||
11 | """`BaseEventLoop` subclass that uses `pvuv` as a backend.""" | |
12 | ||
13 | def _init(self): | |
14 | self._loop = pyuv.Loop() | |
15 | self._async = pyuv.Async(self._loop, self._on_async) | |
16 | self._connection_error = None | |
17 | self._error_stream = None | |
18 | self._callbacks = deque() | |
19 | ||
20 | def _on_connect(self, stream, error): | |
21 | self.stop() | |
22 | if error: | |
23 | msg = 'Cannot connect to {}: {}'.format( | |
24 | self._connect_address, pyuv.errno.strerror(error)) | |
25 | self._connection_error = IOError(msg) | |
26 | return | |
27 | self._read_stream = self._write_stream = stream | |
28 | ||
29 | def _on_read(self, handle, data, error): | |
30 | if error or not data: | |
31 | msg = pyuv.errno.strerror(error) if error else 'EOF' | |
32 | self._on_error(msg) | |
33 | return | |
34 | if handle == self._error_stream: | |
35 | return | |
36 | self._on_data(data) | |
37 | ||
38 | def _on_write(self, handle, error): | |
39 | if error: | |
40 | msg = pyuv.errno.strerror(error) | |
41 | self._on_error(msg) | |
42 | ||
43 | def _on_exit(self, handle, exit_status, term_signal): | |
44 | self._on_error('EOF') | |
45 | ||
46 | def _disconnected(self, *args): | |
47 | raise IOError('Not connected to Nvim') | |
48 | ||
49 | def _connect_tcp(self, address, port): | |
50 | stream = pyuv.TCP(self._loop) | |
51 | self._connect_address = '{}:{}'.format(address, port) | |
52 | stream.connect((address, port), self._on_connect) | |
53 | ||
54 | def _connect_socket(self, path): | |
55 | stream = pyuv.Pipe(self._loop) | |
56 | self._connect_address = path | |
57 | stream.connect(path, self._on_connect) | |
58 | ||
59 | def _connect_stdio(self): | |
60 | self._read_stream = pyuv.Pipe(self._loop) | |
61 | self._read_stream.open(sys.stdin.fileno()) | |
62 | self._write_stream = pyuv.Pipe(self._loop) | |
63 | self._write_stream.open(sys.stdout.fileno()) | |
64 | ||
65 | def _connect_child(self, argv): | |
66 | self._write_stream = pyuv.Pipe(self._loop) | |
67 | self._read_stream = pyuv.Pipe(self._loop) | |
68 | self._error_stream = pyuv.Pipe(self._loop) | |
69 | stdin = pyuv.StdIO(self._write_stream, | |
70 | flags=pyuv.UV_CREATE_PIPE + pyuv.UV_READABLE_PIPE) | |
71 | stdout = pyuv.StdIO(self._read_stream, | |
72 | flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE) | |
73 | stderr = pyuv.StdIO(self._error_stream, | |
74 | flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE) | |
75 | pyuv.Process.spawn(self._loop, | |
76 | args=argv, | |
77 | exit_callback=self._on_exit, | |
78 | flags=pyuv.UV_PROCESS_WINDOWS_HIDE, | |
79 | stdio=(stdin, stdout, stderr,)) | |
80 | self._error_stream.start_read(self._on_read) | |
81 | ||
82 | def _start_reading(self): | |
83 | if self._transport_type in ['tcp', 'socket']: | |
84 | self._loop.run() | |
85 | if self._connection_error: | |
86 | self.run = self.send = self._disconnected | |
87 | raise self._connection_error | |
88 | self._read_stream.start_read(self._on_read) | |
89 | ||
90 | def _send(self, data): | |
91 | self._write_stream.write(data, self._on_write) | |
92 | ||
93 | def _run(self): | |
94 | self._loop.run(pyuv.UV_RUN_DEFAULT) | |
95 | ||
96 | def _stop(self): | |
97 | self._loop.stop() | |
98 | ||
99 | def _close(self): | |
100 | pass | |
101 | ||
102 | def _threadsafe_call(self, fn): | |
103 | self._callbacks.append(fn) | |
104 | self._async.send() | |
105 | ||
106 | def _on_async(self, handle): | |
107 | while self._callbacks: | |
108 | self._callbacks.popleft()() | |
109 | ||
110 | def _setup_signals(self, signals): | |
111 | self._signal_handles = [] | |
112 | ||
113 | def handler(h, signum): | |
114 | self._on_signal(signum) | |
115 | ||
116 | for signum in signals: | |
117 | handle = pyuv.Signal(self._loop) | |
118 | handle.start(handler, signum) | |
119 | self._signal_handles.append(handle) | |
120 | ||
121 | def _teardown_signals(self): | |
122 | for handle in self._signal_handles: | |
123 | handle.stop() |
0 | """Msgpack handling in the event loop pipeline.""" | |
1 | import logging | |
2 | ||
3 | from msgpack import Packer, Unpacker | |
4 | ||
5 | from ..compat import unicode_errors_default | |
6 | ||
7 | logger = logging.getLogger(__name__) | |
8 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
9 | ||
10 | ||
11 | class MsgpackStream(object): | |
12 | ||
13 | """Two-way msgpack stream that wraps a event loop byte stream. | |
14 | ||
15 | This wraps the event loop interface for reading/writing bytes and | |
16 | exposes an interface for reading/writing msgpack documents. | |
17 | """ | |
18 | ||
19 | def __init__(self, event_loop): | |
20 | """Wrap `event_loop` on a msgpack-aware interface.""" | |
21 | self.loop = event_loop | |
22 | self._packer = Packer(encoding='utf-8', | |
23 | unicode_errors=unicode_errors_default) | |
24 | self._unpacker = Unpacker() | |
25 | self._message_cb = None | |
26 | ||
27 | def threadsafe_call(self, fn): | |
28 | """Wrapper around `BaseEventLoop.threadsafe_call`.""" | |
29 | self.loop.threadsafe_call(fn) | |
30 | ||
31 | def send(self, msg): | |
32 | """Queue `msg` for sending to Nvim.""" | |
33 | debug('sent %s', msg) | |
34 | self.loop.send(self._packer.pack(msg)) | |
35 | ||
36 | def run(self, message_cb): | |
37 | """Run the event loop to receive messages from Nvim. | |
38 | ||
39 | While the event loop is running, `message_cb` will be called whenever | |
40 | a message has been successfully parsed from the input stream. | |
41 | """ | |
42 | self._message_cb = message_cb | |
43 | self.loop.run(self._on_data) | |
44 | self._message_cb = None | |
45 | ||
46 | def stop(self): | |
47 | """Stop the event loop.""" | |
48 | self.loop.stop() | |
49 | ||
50 | def close(self): | |
51 | """Close the event loop.""" | |
52 | self.loop.close() | |
53 | ||
54 | def _on_data(self, data): | |
55 | self._unpacker.feed(data) | |
56 | while True: | |
57 | try: | |
58 | debug('waiting for message...') | |
59 | msg = next(self._unpacker) | |
60 | debug('received message: %s', msg) | |
61 | self._message_cb(msg) | |
62 | except StopIteration: | |
63 | debug('unpacker needs more data...') | |
64 | break |
0 | """Synchronous msgpack-rpc session layer.""" | |
1 | import logging | |
2 | import threading | |
3 | from collections import deque | |
4 | from traceback import format_exc | |
5 | ||
6 | import greenlet | |
7 | ||
8 | from ..compat import check_async | |
9 | ||
10 | logger = logging.getLogger(__name__) | |
11 | error, debug, info, warn = (logger.error, logger.debug, logger.info, | |
12 | logger.warning,) | |
13 | ||
14 | ||
15 | class Session(object): | |
16 | ||
17 | """Msgpack-rpc session layer that uses coroutines for a synchronous API. | |
18 | ||
19 | This class provides the public msgpack-rpc API required by this library. | |
20 | It uses the greenlet module to handle requests and notifications coming | |
21 | from Nvim with a synchronous API. | |
22 | """ | |
23 | ||
24 | def __init__(self, async_session): | |
25 | """Wrap `async_session` on a synchronous msgpack-rpc interface.""" | |
26 | self._async_session = async_session | |
27 | self._request_cb = self._notification_cb = None | |
28 | self._pending_messages = deque() | |
29 | self._is_running = False | |
30 | self._setup_exception = None | |
31 | self.loop = async_session.loop | |
32 | self._loop_thread = None | |
33 | ||
34 | def threadsafe_call(self, fn, *args, **kwargs): | |
35 | """Wrapper around `AsyncSession.threadsafe_call`.""" | |
36 | def handler(): | |
37 | try: | |
38 | fn(*args, **kwargs) | |
39 | except Exception: | |
40 | warn("error caught while excecuting async callback\n%s\n", | |
41 | format_exc()) | |
42 | ||
43 | def greenlet_wrapper(): | |
44 | gr = greenlet.greenlet(handler) | |
45 | gr.switch() | |
46 | ||
47 | self._async_session.threadsafe_call(greenlet_wrapper) | |
48 | ||
49 | def next_message(self): | |
50 | """Block until a message(request or notification) is available. | |
51 | ||
52 | If any messages were previously enqueued, return the first in queue. | |
53 | If not, run the event loop until one is received. | |
54 | """ | |
55 | if self._is_running: | |
56 | raise Exception('Event loop already running') | |
57 | if self._pending_messages: | |
58 | return self._pending_messages.popleft() | |
59 | self._async_session.run(self._enqueue_request_and_stop, | |
60 | self._enqueue_notification_and_stop) | |
61 | if self._pending_messages: | |
62 | return self._pending_messages.popleft() | |
63 | ||
64 | def request(self, method, *args, **kwargs): | |
65 | """Send a msgpack-rpc request and block until as response is received. | |
66 | ||
67 | If the event loop is running, this method must have been called by a | |
68 | request or notification handler running on a greenlet. In that case, | |
69 | send the quest and yield to the parent greenlet until a response is | |
70 | available. | |
71 | ||
72 | When the event loop is not running, it will perform a blocking request | |
73 | like this: | |
74 | - Send the request | |
75 | - Run the loop until the response is available | |
76 | - Put requests/notifications received while waiting into a queue | |
77 | ||
78 | If the `async_` flag is present and True, a asynchronous notification | |
79 | is sent instead. This will never block, and the return value or error | |
80 | is ignored. | |
81 | """ | |
82 | async_ = check_async(kwargs.pop('async_', None), kwargs, False) | |
83 | if async_: | |
84 | self._async_session.notify(method, args) | |
85 | return | |
86 | ||
87 | if kwargs: | |
88 | raise ValueError("request got unsupported keyword argument(s): {}" | |
89 | .format(', '.join(kwargs.keys()))) | |
90 | ||
91 | if self._is_running: | |
92 | v = self._yielding_request(method, args) | |
93 | else: | |
94 | v = self._blocking_request(method, args) | |
95 | if not v: | |
96 | # EOF | |
97 | raise IOError('EOF') | |
98 | err, rv = v | |
99 | if err: | |
100 | info("'Received error: %s", err) | |
101 | raise self.error_wrapper(err) | |
102 | return rv | |
103 | ||
104 | def run(self, request_cb, notification_cb, setup_cb=None): | |
105 | """Run the event loop to receive requests and notifications from Nvim. | |
106 | ||
107 | Like `AsyncSession.run()`, but `request_cb` and `notification_cb` are | |
108 | inside greenlets. | |
109 | """ | |
110 | self._request_cb = request_cb | |
111 | self._notification_cb = notification_cb | |
112 | self._is_running = True | |
113 | self._setup_exception = None | |
114 | self._loop_thread = threading.current_thread() | |
115 | ||
116 | def on_setup(): | |
117 | try: | |
118 | setup_cb() | |
119 | except Exception as e: | |
120 | self._setup_exception = e | |
121 | self.stop() | |
122 | ||
123 | if setup_cb: | |
124 | # Create a new greenlet to handle the setup function | |
125 | gr = greenlet.greenlet(on_setup) | |
126 | gr.switch() | |
127 | ||
128 | if self._setup_exception: | |
129 | error('Setup error: {}'.format(self._setup_exception)) | |
130 | raise self._setup_exception | |
131 | ||
132 | # Process all pending requests and notifications | |
133 | while self._pending_messages: | |
134 | msg = self._pending_messages.popleft() | |
135 | getattr(self, '_on_{}'.format(msg[0]))(*msg[1:]) | |
136 | self._async_session.run(self._on_request, self._on_notification) | |
137 | self._is_running = False | |
138 | self._request_cb = None | |
139 | self._notification_cb = None | |
140 | self._loop_thread = None | |
141 | ||
142 | if self._setup_exception: | |
143 | raise self._setup_exception | |
144 | ||
145 | def stop(self): | |
146 | """Stop the event loop.""" | |
147 | self._async_session.stop() | |
148 | ||
149 | def close(self): | |
150 | """Close the event loop.""" | |
151 | self._async_session.close() | |
152 | ||
153 | def _yielding_request(self, method, args): | |
154 | gr = greenlet.getcurrent() | |
155 | parent = gr.parent | |
156 | ||
157 | def response_cb(err, rv): | |
158 | debug('response is available for greenlet %s, switching back', gr) | |
159 | gr.switch(err, rv) | |
160 | ||
161 | self._async_session.request(method, args, response_cb) | |
162 | debug('yielding from greenlet %s to wait for response', gr) | |
163 | return parent.switch() | |
164 | ||
165 | def _blocking_request(self, method, args): | |
166 | result = [] | |
167 | ||
168 | def response_cb(err, rv): | |
169 | result.extend([err, rv]) | |
170 | self.stop() | |
171 | ||
172 | self._async_session.request(method, args, response_cb) | |
173 | self._async_session.run(self._enqueue_request, | |
174 | self._enqueue_notification) | |
175 | return result | |
176 | ||
177 | def _enqueue_request_and_stop(self, name, args, response): | |
178 | self._enqueue_request(name, args, response) | |
179 | self.stop() | |
180 | ||
181 | def _enqueue_notification_and_stop(self, name, args): | |
182 | self._enqueue_notification(name, args) | |
183 | self.stop() | |
184 | ||
185 | def _enqueue_request(self, name, args, response): | |
186 | self._pending_messages.append(('request', name, args, response,)) | |
187 | ||
188 | def _enqueue_notification(self, name, args): | |
189 | self._pending_messages.append(('notification', name, args,)) | |
190 | ||
191 | def _on_request(self, name, args, response): | |
192 | def handler(): | |
193 | try: | |
194 | rv = self._request_cb(name, args) | |
195 | debug('greenlet %s finished executing, ' + | |
196 | 'sending %s as response', gr, rv) | |
197 | response.send(rv) | |
198 | except ErrorResponse as err: | |
199 | warn("error response from request '%s %s': %s", name, | |
200 | args, format_exc()) | |
201 | response.send(err.args[0], error=True) | |
202 | except Exception as err: | |
203 | warn("error caught while processing request '%s %s': %s", name, | |
204 | args, format_exc()) | |
205 | response.send(repr(err) + "\n" + format_exc(5), error=True) | |
206 | debug('greenlet %s is now dying...', gr) | |
207 | ||
208 | # Create a new greenlet to handle the request | |
209 | gr = greenlet.greenlet(handler) | |
210 | debug('received rpc request, greenlet %s will handle it', gr) | |
211 | gr.switch() | |
212 | ||
213 | def _on_notification(self, name, args): | |
214 | def handler(): | |
215 | try: | |
216 | self._notification_cb(name, args) | |
217 | debug('greenlet %s finished executing', gr) | |
218 | except Exception: | |
219 | warn("error caught while processing notification '%s %s': %s", | |
220 | name, args, format_exc()) | |
221 | ||
222 | debug('greenlet %s is now dying...', gr) | |
223 | ||
224 | gr = greenlet.greenlet(handler) | |
225 | debug('received rpc notification, greenlet %s will handle it', gr) | |
226 | gr.switch() | |
227 | ||
228 | ||
229 | class ErrorResponse(BaseException): | |
230 | ||
231 | """Raise this in a request handler to respond with a given error message. | |
232 | ||
233 | Unlike when other exceptions are caught, this gives full control off the | |
234 | error response sent. When "ErrorResponse(msg)" is caught "msg" will be | |
235 | sent verbatim as the error response.No traceback will be appended. | |
236 | """ | |
237 | ||
238 | pass |
0 | """Nvim plugin/host subpackage.""" | |
1 | ||
2 | from .decorators import (autocmd, command, decode, encoding, function, | |
3 | plugin, rpc_export, shutdown_hook) | |
4 | from .host import Host | |
5 | ||
6 | ||
7 | __all__ = ('Host', 'plugin', 'rpc_export', 'command', 'autocmd', | |
8 | 'function', 'encoding', 'decode', 'shutdown_hook') |
0 | """Decorators used by python host plugin system.""" | |
1 | ||
2 | import inspect | |
3 | import logging | |
4 | ||
5 | from ..compat import IS_PYTHON3, unicode_errors_default | |
6 | ||
7 | logger = logging.getLogger(__name__) | |
8 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
9 | __all__ = ('plugin', 'rpc_export', 'command', 'autocmd', 'function', | |
10 | 'encoding', 'decode', 'shutdown_hook') | |
11 | ||
12 | ||
13 | def plugin(cls): | |
14 | """Tag a class as a plugin. | |
15 | ||
16 | This decorator is required to make the class methods discoverable by the | |
17 | plugin_load method of the host. | |
18 | """ | |
19 | cls._nvim_plugin = True | |
20 | # the _nvim_bind attribute is set to True by default, meaning that | |
21 | # decorated functions have a bound Nvim instance as first argument. | |
22 | # For methods in a plugin-decorated class this is not required, because | |
23 | # the class initializer will already receive the nvim object. | |
24 | predicate = lambda fn: hasattr(fn, '_nvim_bind') | |
25 | for _, fn in inspect.getmembers(cls, predicate): | |
26 | if IS_PYTHON3: | |
27 | fn._nvim_bind = False | |
28 | else: | |
29 | fn.im_func._nvim_bind = False | |
30 | return cls | |
31 | ||
32 | ||
33 | def rpc_export(rpc_method_name, sync=False): | |
34 | """Export a function or plugin method as a msgpack-rpc request handler.""" | |
35 | def dec(f): | |
36 | f._nvim_rpc_method_name = rpc_method_name | |
37 | f._nvim_rpc_sync = sync | |
38 | f._nvim_bind = True | |
39 | f._nvim_prefix_plugin_path = False | |
40 | return f | |
41 | return dec | |
42 | ||
43 | ||
44 | def command(name, nargs=0, complete=None, range=None, count=None, bang=False, | |
45 | register=False, sync=False, allow_nested=False, eval=None): | |
46 | """Tag a function or plugin method as a Nvim command handler.""" | |
47 | def dec(f): | |
48 | f._nvim_rpc_method_name = 'command:{}'.format(name) | |
49 | f._nvim_rpc_sync = sync | |
50 | f._nvim_bind = True | |
51 | f._nvim_prefix_plugin_path = True | |
52 | ||
53 | opts = {} | |
54 | ||
55 | if range is not None: | |
56 | opts['range'] = '' if range is True else str(range) | |
57 | elif count is not None: | |
58 | opts['count'] = count | |
59 | ||
60 | if bang: | |
61 | opts['bang'] = '' | |
62 | ||
63 | if register: | |
64 | opts['register'] = '' | |
65 | ||
66 | if nargs: | |
67 | opts['nargs'] = nargs | |
68 | ||
69 | if complete: | |
70 | opts['complete'] = complete | |
71 | ||
72 | if eval: | |
73 | opts['eval'] = eval | |
74 | ||
75 | if not sync and allow_nested: | |
76 | rpc_sync = "urgent" | |
77 | else: | |
78 | rpc_sync = sync | |
79 | ||
80 | f._nvim_rpc_spec = { | |
81 | 'type': 'command', | |
82 | 'name': name, | |
83 | 'sync': rpc_sync, | |
84 | 'opts': opts | |
85 | } | |
86 | return f | |
87 | return dec | |
88 | ||
89 | ||
90 | def autocmd(name, pattern='*', sync=False, allow_nested=False, eval=None): | |
91 | """Tag a function or plugin method as a Nvim autocommand handler.""" | |
92 | def dec(f): | |
93 | f._nvim_rpc_method_name = 'autocmd:{}:{}'.format(name, pattern) | |
94 | f._nvim_rpc_sync = sync | |
95 | f._nvim_bind = True | |
96 | f._nvim_prefix_plugin_path = True | |
97 | ||
98 | opts = { | |
99 | 'pattern': pattern | |
100 | } | |
101 | ||
102 | if eval: | |
103 | opts['eval'] = eval | |
104 | ||
105 | if not sync and allow_nested: | |
106 | rpc_sync = "urgent" | |
107 | else: | |
108 | rpc_sync = sync | |
109 | ||
110 | f._nvim_rpc_spec = { | |
111 | 'type': 'autocmd', | |
112 | 'name': name, | |
113 | 'sync': rpc_sync, | |
114 | 'opts': opts | |
115 | } | |
116 | return f | |
117 | return dec | |
118 | ||
119 | ||
120 | def function(name, range=False, sync=False, allow_nested=False, eval=None): | |
121 | """Tag a function or plugin method as a Nvim function handler.""" | |
122 | def dec(f): | |
123 | f._nvim_rpc_method_name = 'function:{}'.format(name) | |
124 | f._nvim_rpc_sync = sync | |
125 | f._nvim_bind = True | |
126 | f._nvim_prefix_plugin_path = True | |
127 | ||
128 | opts = {} | |
129 | ||
130 | if range: | |
131 | opts['range'] = '' if range is True else str(range) | |
132 | ||
133 | if eval: | |
134 | opts['eval'] = eval | |
135 | ||
136 | if not sync and allow_nested: | |
137 | rpc_sync = "urgent" | |
138 | else: | |
139 | rpc_sync = sync | |
140 | ||
141 | f._nvim_rpc_spec = { | |
142 | 'type': 'function', | |
143 | 'name': name, | |
144 | 'sync': rpc_sync, | |
145 | 'opts': opts | |
146 | } | |
147 | return f | |
148 | return dec | |
149 | ||
150 | ||
151 | def shutdown_hook(f): | |
152 | """Tag a function or method as a shutdown hook.""" | |
153 | f._nvim_shutdown_hook = True | |
154 | f._nvim_bind = True | |
155 | return f | |
156 | ||
157 | ||
158 | def decode(mode=unicode_errors_default): | |
159 | """Configure automatic encoding/decoding of strings.""" | |
160 | def dec(f): | |
161 | f._nvim_decode = mode | |
162 | return f | |
163 | return dec | |
164 | ||
165 | ||
166 | def encoding(encoding=True): | |
167 | """DEPRECATED: use neovim.decode().""" | |
168 | if isinstance(encoding, str): | |
169 | encoding = True | |
170 | ||
171 | def dec(f): | |
172 | f._nvim_decode = encoding | |
173 | return f | |
174 | return dec |
0 | """Implements a Nvim host for python plugins.""" | |
1 | import imp | |
2 | import inspect | |
3 | import logging | |
4 | import os | |
5 | import os.path | |
6 | import re | |
7 | import sys | |
8 | from functools import partial | |
9 | from traceback import format_exc | |
10 | ||
11 | from . import script_host | |
12 | from ..api import decode_if_bytes, walk | |
13 | from ..compat import IS_PYTHON3, find_module | |
14 | from ..msgpack_rpc import ErrorResponse | |
15 | from ..util import VERSION, format_exc_skip | |
16 | ||
17 | __all__ = ('Host') | |
18 | ||
19 | logger = logging.getLogger(__name__) | |
20 | error, debug, info, warn = (logger.error, logger.debug, logger.info, | |
21 | logger.warning,) | |
22 | ||
23 | host_method_spec = {"poll": {}, "specs": {"nargs": 1}, "shutdown": {}} | |
24 | ||
25 | ||
26 | class Host(object): | |
27 | ||
28 | """Nvim host for python plugins. | |
29 | ||
30 | Takes care of loading/unloading plugins and routing msgpack-rpc | |
31 | requests/notifications to the appropriate handlers. | |
32 | """ | |
33 | ||
34 | def __init__(self, nvim): | |
35 | """Set handlers for plugin_load/plugin_unload.""" | |
36 | self.nvim = nvim | |
37 | self._specs = {} | |
38 | self._loaded = {} | |
39 | self._load_errors = {} | |
40 | self._notification_handlers = {} | |
41 | self._request_handlers = { | |
42 | 'poll': lambda: 'ok', | |
43 | 'specs': self._on_specs_request, | |
44 | 'shutdown': self.shutdown | |
45 | } | |
46 | ||
47 | # Decode per default for Python3 | |
48 | self._decode_default = IS_PYTHON3 | |
49 | ||
50 | def _on_async_err(self, msg): | |
51 | self.nvim.err_write(msg, async_=True) | |
52 | ||
53 | def start(self, plugins): | |
54 | """Start listening for msgpack-rpc requests and notifications.""" | |
55 | self.nvim.run_loop(self._on_request, | |
56 | self._on_notification, | |
57 | lambda: self._load(plugins), | |
58 | err_cb=self._on_async_err) | |
59 | ||
60 | def shutdown(self): | |
61 | """Shutdown the host.""" | |
62 | self._unload() | |
63 | self.nvim.stop_loop() | |
64 | ||
65 | def _wrap_function(self, fn, sync, decode, nvim_bind, name, *args): | |
66 | if decode: | |
67 | args = walk(decode_if_bytes, args, decode) | |
68 | if nvim_bind is not None: | |
69 | args.insert(0, nvim_bind) | |
70 | try: | |
71 | return fn(*args) | |
72 | except Exception: | |
73 | if sync: | |
74 | msg = ("error caught in request handler '{} {}':\n{}" | |
75 | .format(name, args, format_exc_skip(1))) | |
76 | raise ErrorResponse(msg) | |
77 | else: | |
78 | msg = ("error caught in async handler '{} {}'\n{}\n" | |
79 | .format(name, args, format_exc_skip(1))) | |
80 | self._on_async_err(msg + "\n") | |
81 | ||
82 | def _on_request(self, name, args): | |
83 | """Handle a msgpack-rpc request.""" | |
84 | if IS_PYTHON3: | |
85 | name = decode_if_bytes(name) | |
86 | handler = self._request_handlers.get(name, None) | |
87 | if not handler: | |
88 | msg = self._missing_handler_error(name, 'request') | |
89 | error(msg) | |
90 | raise ErrorResponse(msg) | |
91 | ||
92 | debug('calling request handler for "%s", args: "%s"', name, args) | |
93 | rv = handler(*args) | |
94 | debug("request handler for '%s %s' returns: %s", name, args, rv) | |
95 | return rv | |
96 | ||
97 | def _on_notification(self, name, args): | |
98 | """Handle a msgpack-rpc notification.""" | |
99 | if IS_PYTHON3: | |
100 | name = decode_if_bytes(name) | |
101 | handler = self._notification_handlers.get(name, None) | |
102 | if not handler: | |
103 | msg = self._missing_handler_error(name, 'notification') | |
104 | error(msg) | |
105 | self._on_async_err(msg + "\n") | |
106 | return | |
107 | ||
108 | debug('calling notification handler for "%s", args: "%s"', name, args) | |
109 | handler(*args) | |
110 | ||
111 | def _missing_handler_error(self, name, kind): | |
112 | msg = 'no {} handler registered for "{}"'.format(kind, name) | |
113 | pathmatch = re.match(r'(.+):[^:]+:[^:]+', name) | |
114 | if pathmatch: | |
115 | loader_error = self._load_errors.get(pathmatch.group(1)) | |
116 | if loader_error is not None: | |
117 | msg = msg + "\n" + loader_error | |
118 | return msg | |
119 | ||
120 | def _load(self, plugins): | |
121 | has_script = False | |
122 | for path in plugins: | |
123 | err = None | |
124 | if path in self._loaded: | |
125 | error('{} is already loaded'.format(path)) | |
126 | continue | |
127 | try: | |
128 | if path == "script_host.py": | |
129 | module = script_host | |
130 | has_script = True | |
131 | else: | |
132 | directory, name = os.path.split(os.path.splitext(path)[0]) | |
133 | file, pathname, descr = find_module(name, [directory]) | |
134 | module = imp.load_module(name, file, pathname, descr) | |
135 | handlers = [] | |
136 | self._discover_classes(module, handlers, path) | |
137 | self._discover_functions(module, handlers, path) | |
138 | if not handlers: | |
139 | error('{} exports no handlers'.format(path)) | |
140 | continue | |
141 | self._loaded[path] = {'handlers': handlers, 'module': module} | |
142 | except Exception as e: | |
143 | err = ('Encountered {} loading plugin at {}: {}\n{}' | |
144 | .format(type(e).__name__, path, e, format_exc(5))) | |
145 | error(err) | |
146 | self._load_errors[path] = err | |
147 | ||
148 | if len(plugins) == 1 and has_script: | |
149 | kind = "script" | |
150 | else: | |
151 | kind = "rplugin" | |
152 | name = "python{}-{}-host".format(sys.version_info[0], kind) | |
153 | attributes = {"license": "Apache v2", | |
154 | "website": "github.com/neovim/python-client"} | |
155 | self.nvim.api.set_client_info( | |
156 | name, VERSION.__dict__, "host", host_method_spec, | |
157 | attributes, async_=True) | |
158 | ||
159 | def _unload(self): | |
160 | for path, plugin in self._loaded.items(): | |
161 | handlers = plugin['handlers'] | |
162 | for handler in handlers: | |
163 | method_name = handler._nvim_rpc_method_name | |
164 | if hasattr(handler, '_nvim_shutdown_hook'): | |
165 | handler() | |
166 | elif handler._nvim_rpc_sync: | |
167 | del self._request_handlers[method_name] | |
168 | else: | |
169 | del self._notification_handlers[method_name] | |
170 | self._specs = {} | |
171 | self._loaded = {} | |
172 | ||
173 | def _discover_classes(self, module, handlers, plugin_path): | |
174 | for _, cls in inspect.getmembers(module, inspect.isclass): | |
175 | if getattr(cls, '_nvim_plugin', False): | |
176 | # create an instance of the plugin and pass the nvim object | |
177 | plugin = cls(self._configure_nvim_for(cls)) | |
178 | # discover handlers in the plugin instance | |
179 | self._discover_functions(plugin, handlers, plugin_path) | |
180 | ||
181 | def _discover_functions(self, obj, handlers, plugin_path): | |
182 | def predicate(o): | |
183 | return hasattr(o, '_nvim_rpc_method_name') | |
184 | ||
185 | specs = [] | |
186 | objdecode = getattr(obj, '_nvim_decode', self._decode_default) | |
187 | for _, fn in inspect.getmembers(obj, predicate): | |
188 | sync = fn._nvim_rpc_sync | |
189 | decode = getattr(fn, '_nvim_decode', objdecode) | |
190 | nvim_bind = None | |
191 | if fn._nvim_bind: | |
192 | nvim_bind = self._configure_nvim_for(fn) | |
193 | ||
194 | method = fn._nvim_rpc_method_name | |
195 | if fn._nvim_prefix_plugin_path: | |
196 | method = '{}:{}'.format(plugin_path, method) | |
197 | ||
198 | fn_wrapped = partial(self._wrap_function, fn, | |
199 | sync, decode, nvim_bind, method) | |
200 | self._copy_attributes(fn, fn_wrapped) | |
201 | # register in the rpc handler dict | |
202 | if sync: | |
203 | if method in self._request_handlers: | |
204 | raise Exception(('Request handler for "{}" is ' + | |
205 | 'already registered').format(method)) | |
206 | self._request_handlers[method] = fn_wrapped | |
207 | else: | |
208 | if method in self._notification_handlers: | |
209 | raise Exception(('Notification handler for "{}" is ' + | |
210 | 'already registered').format(method)) | |
211 | self._notification_handlers[method] = fn_wrapped | |
212 | if hasattr(fn, '_nvim_rpc_spec'): | |
213 | specs.append(fn._nvim_rpc_spec) | |
214 | handlers.append(fn_wrapped) | |
215 | if specs: | |
216 | self._specs[plugin_path] = specs | |
217 | ||
218 | def _copy_attributes(self, fn, fn2): | |
219 | # Copy _nvim_* attributes from the original function | |
220 | for attr in dir(fn): | |
221 | if attr.startswith('_nvim_'): | |
222 | setattr(fn2, attr, getattr(fn, attr)) | |
223 | ||
224 | def _on_specs_request(self, path): | |
225 | if IS_PYTHON3: | |
226 | path = decode_if_bytes(path) | |
227 | if path in self._load_errors: | |
228 | self.nvim.out_write(self._load_errors[path] + '\n') | |
229 | return self._specs.get(path, 0) | |
230 | ||
231 | def _configure_nvim_for(self, obj): | |
232 | # Configure a nvim instance for obj (checks encoding configuration) | |
233 | nvim = self.nvim | |
234 | decode = getattr(obj, '_nvim_decode', self._decode_default) | |
235 | if decode: | |
236 | nvim = nvim.with_decode(decode) | |
237 | return nvim |
0 | """Legacy python/python3-vim emulation.""" | |
1 | import imp | |
2 | import io | |
3 | import logging | |
4 | import os | |
5 | import sys | |
6 | ||
7 | from .decorators import plugin, rpc_export | |
8 | from ..api import Nvim, walk | |
9 | from ..msgpack_rpc import ErrorResponse | |
10 | from ..util import format_exc_skip | |
11 | ||
12 | __all__ = ('ScriptHost',) | |
13 | ||
14 | ||
15 | logger = logging.getLogger(__name__) | |
16 | debug, info, warn = (logger.debug, logger.info, logger.warn,) | |
17 | ||
18 | IS_PYTHON3 = sys.version_info >= (3, 0) | |
19 | ||
20 | if IS_PYTHON3: | |
21 | basestring = str | |
22 | ||
23 | if sys.version_info >= (3, 4): | |
24 | from importlib.machinery import PathFinder | |
25 | ||
26 | PYTHON_SUBDIR = 'python3' | |
27 | else: | |
28 | PYTHON_SUBDIR = 'python2' | |
29 | ||
30 | ||
31 | @plugin | |
32 | class ScriptHost(object): | |
33 | ||
34 | """Provides an environment for running python plugins created for Vim.""" | |
35 | ||
36 | def __init__(self, nvim): | |
37 | """Initialize the legacy python-vim environment.""" | |
38 | self.setup(nvim) | |
39 | # context where all code will run | |
40 | self.module = imp.new_module('__main__') | |
41 | nvim.script_context = self.module | |
42 | # it seems some plugins assume 'sys' is already imported, so do it now | |
43 | exec('import sys', self.module.__dict__) | |
44 | self.legacy_vim = LegacyVim.from_nvim(nvim) | |
45 | sys.modules['vim'] = self.legacy_vim | |
46 | ||
47 | # Handle DirChanged. #296 | |
48 | nvim.command( | |
49 | 'au DirChanged * call rpcnotify({}, "python_chdir", v:event.cwd)' | |
50 | .format(nvim.channel_id), async_=True) | |
51 | # XXX: Avoid race condition. | |
52 | # https://github.com/neovim/python-client/pull/296#issuecomment-358970531 | |
53 | # TODO(bfredl): when host initialization has been refactored, | |
54 | # to make __init__ safe again, the following should work: | |
55 | # os.chdir(nvim.eval('getcwd()', async_=False)) | |
56 | nvim.command('call rpcnotify({}, "python_chdir", getcwd())' | |
57 | .format(nvim.channel_id), async_=True) | |
58 | ||
59 | def setup(self, nvim): | |
60 | """Setup import hooks and global streams. | |
61 | ||
62 | This will add import hooks for importing modules from runtime | |
63 | directories and patch the sys module so 'print' calls will be | |
64 | forwarded to Nvim. | |
65 | """ | |
66 | self.nvim = nvim | |
67 | info('install import hook/path') | |
68 | self.hook = path_hook(nvim) | |
69 | sys.path_hooks.append(self.hook) | |
70 | nvim.VIM_SPECIAL_PATH = '_vim_path_' | |
71 | sys.path.append(nvim.VIM_SPECIAL_PATH) | |
72 | info('redirect sys.stdout and sys.stderr') | |
73 | self.saved_stdout = sys.stdout | |
74 | self.saved_stderr = sys.stderr | |
75 | sys.stdout = RedirectStream(lambda data: nvim.out_write(data)) | |
76 | sys.stderr = RedirectStream(lambda data: nvim.err_write(data)) | |
77 | ||
78 | def teardown(self): | |
79 | """Restore state modified from the `setup` call.""" | |
80 | nvim = self.nvim | |
81 | info('uninstall import hook/path') | |
82 | sys.path.remove(nvim.VIM_SPECIAL_PATH) | |
83 | sys.path_hooks.remove(self.hook) | |
84 | info('restore sys.stdout and sys.stderr') | |
85 | sys.stdout = self.saved_stdout | |
86 | sys.stderr = self.saved_stderr | |
87 | ||
88 | @rpc_export('python_execute', sync=True) | |
89 | def python_execute(self, script, range_start, range_stop): | |
90 | """Handle the `python` ex command.""" | |
91 | self._set_current_range(range_start, range_stop) | |
92 | try: | |
93 | exec(script, self.module.__dict__) | |
94 | except Exception: | |
95 | raise ErrorResponse(format_exc_skip(1)) | |
96 | ||
97 | @rpc_export('python_execute_file', sync=True) | |
98 | def python_execute_file(self, file_path, range_start, range_stop): | |
99 | """Handle the `pyfile` ex command.""" | |
100 | self._set_current_range(range_start, range_stop) | |
101 | with open(file_path) as f: | |
102 | script = compile(f.read(), file_path, 'exec') | |
103 | try: | |
104 | exec(script, self.module.__dict__) | |
105 | except Exception: | |
106 | raise ErrorResponse(format_exc_skip(1)) | |
107 | ||
108 | @rpc_export('python_do_range', sync=True) | |
109 | def python_do_range(self, start, stop, code): | |
110 | """Handle the `pydo` ex command.""" | |
111 | self._set_current_range(start, stop) | |
112 | nvim = self.nvim | |
113 | start -= 1 | |
114 | fname = '_vim_pydo' | |
115 | ||
116 | # define the function | |
117 | function_def = 'def %s(line, linenr):\n %s' % (fname, code,) | |
118 | exec(function_def, self.module.__dict__) | |
119 | # get the function | |
120 | function = self.module.__dict__[fname] | |
121 | while start < stop: | |
122 | # Process batches of 5000 to avoid the overhead of making multiple | |
123 | # API calls for every line. Assuming an average line length of 100 | |
124 | # bytes, approximately 488 kilobytes will be transferred per batch, | |
125 | # which can be done very quickly in a single API call. | |
126 | sstart = start | |
127 | sstop = min(start + 5000, stop) | |
128 | lines = nvim.current.buffer.api.get_lines(sstart, sstop, True) | |
129 | ||
130 | exception = None | |
131 | newlines = [] | |
132 | linenr = sstart + 1 | |
133 | for i, line in enumerate(lines): | |
134 | result = function(line, linenr) | |
135 | if result is None: | |
136 | # Update earlier lines, and skip to the next | |
137 | if newlines: | |
138 | end = sstart + len(newlines) - 1 | |
139 | nvim.current.buffer.api.set_lines(sstart, end, | |
140 | True, newlines) | |
141 | sstart += len(newlines) + 1 | |
142 | newlines = [] | |
143 | pass | |
144 | elif isinstance(result, basestring): | |
145 | newlines.append(result) | |
146 | else: | |
147 | exception = TypeError('pydo should return a string ' + | |
148 | 'or None, found %s instead' | |
149 | % result.__class__.__name__) | |
150 | break | |
151 | linenr += 1 | |
152 | ||
153 | start = sstop | |
154 | if newlines: | |
155 | end = sstart + len(newlines) | |
156 | nvim.current.buffer.api.set_lines(sstart, end, True, newlines) | |
157 | if exception: | |
158 | raise exception | |
159 | # delete the function | |
160 | del self.module.__dict__[fname] | |
161 | ||
162 | @rpc_export('python_eval', sync=True) | |
163 | def python_eval(self, expr): | |
164 | """Handle the `pyeval` vim function.""" | |
165 | return eval(expr, self.module.__dict__) | |
166 | ||
167 | @rpc_export('python_chdir', sync=False) | |
168 | def python_chdir(self, cwd): | |
169 | """Handle working directory changes.""" | |
170 | os.chdir(cwd) | |
171 | ||
172 | def _set_current_range(self, start, stop): | |
173 | current = self.legacy_vim.current | |
174 | current.range = current.buffer.range(start, stop) | |
175 | ||
176 | ||
177 | class RedirectStream(io.IOBase): | |
178 | def __init__(self, redirect_handler): | |
179 | self.redirect_handler = redirect_handler | |
180 | ||
181 | def write(self, data): | |
182 | self.redirect_handler(data) | |
183 | ||
184 | def writelines(self, seq): | |
185 | self.redirect_handler('\n'.join(seq)) | |
186 | ||
187 | ||
188 | if IS_PYTHON3: | |
189 | num_types = (int, float) | |
190 | else: | |
191 | num_types = (int, long, float) # noqa: F821 | |
192 | ||
193 | ||
194 | def num_to_str(obj): | |
195 | if isinstance(obj, num_types): | |
196 | return str(obj) | |
197 | else: | |
198 | return obj | |
199 | ||
200 | ||
201 | class LegacyVim(Nvim): | |
202 | def eval(self, expr): | |
203 | obj = self.request("vim_eval", expr) | |
204 | return walk(num_to_str, obj) | |
205 | ||
206 | ||
207 | # This was copied/adapted from nvim-python help | |
208 | def path_hook(nvim): | |
209 | def _get_paths(): | |
210 | if nvim._thread_invalid(): | |
211 | return [] | |
212 | return discover_runtime_directories(nvim) | |
213 | ||
214 | def _find_module(fullname, oldtail, path): | |
215 | idx = oldtail.find('.') | |
216 | if idx > 0: | |
217 | name = oldtail[:idx] | |
218 | tail = oldtail[idx + 1:] | |
219 | fmr = imp.find_module(name, path) | |
220 | module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr) | |
221 | return _find_module(fullname, tail, module.__path__) | |
222 | else: | |
223 | return imp.find_module(fullname, path) | |
224 | ||
225 | class VimModuleLoader(object): | |
226 | def __init__(self, module): | |
227 | self.module = module | |
228 | ||
229 | def load_module(self, fullname, path=None): | |
230 | # Check sys.modules, required for reload (see PEP302). | |
231 | try: | |
232 | return sys.modules[fullname] | |
233 | except KeyError: | |
234 | pass | |
235 | return imp.load_module(fullname, *self.module) | |
236 | ||
237 | class VimPathFinder(object): | |
238 | @staticmethod | |
239 | def find_module(fullname, path=None): | |
240 | """Method for Python 2.7 and 3.3.""" | |
241 | try: | |
242 | return VimModuleLoader( | |
243 | _find_module(fullname, fullname, path or _get_paths())) | |
244 | except ImportError: | |
245 | return None | |
246 | ||
247 | @staticmethod | |
248 | def find_spec(fullname, target=None): | |
249 | """Method for Python 3.4+.""" | |
250 | return PathFinder.find_spec(fullname, _get_paths(), target) | |
251 | ||
252 | def hook(path): | |
253 | if path == nvim.VIM_SPECIAL_PATH: | |
254 | return VimPathFinder | |
255 | else: | |
256 | raise ImportError | |
257 | ||
258 | return hook | |
259 | ||
260 | ||
261 | def discover_runtime_directories(nvim): | |
262 | rv = [] | |
263 | for rtp in nvim.list_runtime_paths(): | |
264 | if not os.path.exists(rtp): | |
265 | continue | |
266 | for subdir in ['pythonx', PYTHON_SUBDIR]: | |
267 | path = os.path.join(rtp, subdir) | |
268 | if os.path.exists(path): | |
269 | rv.append(path) | |
270 | return rv |
0 | """Shared utility functions.""" | |
1 | ||
2 | import sys | |
3 | from traceback import format_exception | |
4 | ||
5 | ||
6 | def format_exc_skip(skip, limit=None): | |
7 | """Like traceback.format_exc but allow skipping the first frames.""" | |
8 | type, val, tb = sys.exc_info() | |
9 | for i in range(skip): | |
10 | tb = tb.tb_next | |
11 | return ('\n'.join(format_exception(type, val, tb, limit))).rstrip() | |
12 | ||
13 | ||
14 | # Taken from SimpleNamespace in python 3 | |
15 | class Version: | |
16 | ||
17 | """Helper class for version info.""" | |
18 | ||
19 | def __init__(self, **kwargs): | |
20 | """Create the Version object.""" | |
21 | self.__dict__.update(kwargs) | |
22 | ||
23 | def __repr__(self): | |
24 | """Return str representation of the Version.""" | |
25 | keys = sorted(self.__dict__) | |
26 | items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) | |
27 | return "{}({})".format(type(self).__name__, ", ".join(items)) | |
28 | ||
29 | def __eq__(self, other): | |
30 | """Check if version is same as other.""" | |
31 | return self.__dict__ == other.__dict__ | |
32 | ||
33 | ||
34 | VERSION = Version(major=0, minor=3, patch=0, prerelease='') |
0 | Metadata-Version: 2.1 | |
1 | Name: neovim | |
2 | Version: 0.3.0 | |
3 | Summary: Python client to neovim | |
4 | Home-page: http://github.com/neovim/python-client | |
5 | Author: Thiago de Arruda | |
6 | Author-email: tpadilha84@gmail.com | |
7 | License: Apache | |
8 | Download-URL: https://github.com/neovim/python-client/archive/0.3.0.tar.gz | |
9 | Description: UNKNOWN | |
10 | Platform: UNKNOWN | |
11 | Provides-Extra: pyuv | |
12 | Provides-Extra: test |
0 | LICENSE | |
1 | MANIFEST.in | |
2 | README.md | |
3 | setup.cfg | |
4 | setup.py | |
5 | neovim/__init__.py | |
6 | neovim/compat.py | |
7 | neovim/util.py | |
8 | neovim.egg-info/PKG-INFO | |
9 | neovim.egg-info/SOURCES.txt | |
10 | neovim.egg-info/dependency_links.txt | |
11 | neovim.egg-info/not-zip-safe | |
12 | neovim.egg-info/requires.txt | |
13 | neovim.egg-info/top_level.txt | |
14 | neovim/api/__init__.py | |
15 | neovim/api/buffer.py | |
16 | neovim/api/common.py | |
17 | neovim/api/nvim.py | |
18 | neovim/api/tabpage.py | |
19 | neovim/api/window.py | |
20 | neovim/msgpack_rpc/__init__.py | |
21 | neovim/msgpack_rpc/async_session.py | |
22 | neovim/msgpack_rpc/msgpack_stream.py | |
23 | neovim/msgpack_rpc/session.py | |
24 | neovim/msgpack_rpc/event_loop/__init__.py | |
25 | neovim/msgpack_rpc/event_loop/asyncio.py | |
26 | neovim/msgpack_rpc/event_loop/base.py | |
27 | neovim/msgpack_rpc/event_loop/uv.py | |
28 | neovim/plugin/__init__.py | |
29 | neovim/plugin/decorators.py | |
30 | neovim/plugin/host.py | |
31 | neovim/plugin/script_host.py | |
32 | test/test_buffer.py | |
33 | test/test_client_rpc.py | |
34 | test/test_concurrency.py | |
35 | test/test_decorators.py | |
36 | test/test_events.py | |
37 | test/test_host.py | |
38 | test/test_tabpage.py | |
39 | test/test_vim.py | |
40 | test/test_window.py⏎ |
0 | """Python client for Nvim. | |
1 | ||
2 | Client library for talking with Nvim processes via its msgpack-rpc API. | |
3 | """ | |
4 | import logging | |
5 | import os | |
6 | import sys | |
7 | ||
8 | from .api import Nvim, NvimError | |
9 | from .compat import IS_PYTHON3 | |
10 | from .msgpack_rpc import (ErrorResponse, child_session, socket_session, | |
11 | stdio_session, tcp_session) | |
12 | from .plugin import (Host, autocmd, command, decode, encoding, function, | |
13 | plugin, rpc_export, shutdown_hook) | |
14 | from .util import VERSION, Version | |
15 | ||
16 | ||
17 | __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session', | |
18 | 'start_host', 'autocmd', 'command', 'encoding', 'decode', | |
19 | 'function', 'plugin', 'rpc_export', 'Host', 'Nvim', 'NvimError', | |
20 | 'Version', 'VERSION', 'shutdown_hook', 'attach', 'setup_logging', | |
21 | 'ErrorResponse') | |
22 | ||
23 | ||
24 | def start_host(session=None): | |
25 | """Promote the current process into python plugin host for Nvim. | |
26 | ||
27 | Start msgpack-rpc event loop for `session`, listening for Nvim requests | |
28 | and notifications. It registers Nvim commands for loading/unloading | |
29 | python plugins. | |
30 | ||
31 | The sys.stdout and sys.stderr streams are redirected to Nvim through | |
32 | `session`. That means print statements probably won't work as expected | |
33 | while this function doesn't return. | |
34 | ||
35 | This function is normally called at program startup and could have been | |
36 | defined as a separate executable. It is exposed as a library function for | |
37 | testing purposes only. | |
38 | """ | |
39 | plugins = [] | |
40 | for arg in sys.argv: | |
41 | _, ext = os.path.splitext(arg) | |
42 | if ext == '.py': | |
43 | plugins.append(arg) | |
44 | elif os.path.isdir(arg): | |
45 | init = os.path.join(arg, '__init__.py') | |
46 | if os.path.isfile(init): | |
47 | plugins.append(arg) | |
48 | ||
49 | # This is a special case to support the old workaround of | |
50 | # adding an empty .py file to make a package directory | |
51 | # visible, and it should be removed soon. | |
52 | for path in list(plugins): | |
53 | dup = path + ".py" | |
54 | if os.path.isdir(path) and dup in plugins: | |
55 | plugins.remove(dup) | |
56 | ||
57 | # Special case: the legacy scripthost receives a single relative filename | |
58 | # while the rplugin host will receive absolute paths. | |
59 | if plugins == ["script_host.py"]: | |
60 | name = "script" | |
61 | else: | |
62 | name = "rplugin" | |
63 | ||
64 | setup_logging(name) | |
65 | ||
66 | if not session: | |
67 | session = stdio_session() | |
68 | nvim = Nvim.from_session(session) | |
69 | ||
70 | if nvim.version.api_level < 1: | |
71 | sys.stderr.write("This version of pynvim " | |
72 | "requires nvim 0.1.6 or later") | |
73 | sys.exit(1) | |
74 | ||
75 | host = Host(nvim) | |
76 | host.start(plugins) | |
77 | ||
78 | ||
79 | def attach(session_type, address=None, port=None, | |
80 | path=None, argv=None, decode=None): | |
81 | """Provide a nicer interface to create python api sessions. | |
82 | ||
83 | Previous machinery to create python api sessions is still there. This only | |
84 | creates a facade function to make things easier for the most usual cases. | |
85 | Thus, instead of: | |
86 | from pynvim import socket_session, Nvim | |
87 | session = tcp_session(address=<address>, port=<port>) | |
88 | nvim = Nvim.from_session(session) | |
89 | You can now do: | |
90 | from pynvim import attach | |
91 | nvim = attach('tcp', address=<address>, port=<port>) | |
92 | And also: | |
93 | nvim = attach('socket', path=<path>) | |
94 | nvim = attach('child', argv=<argv>) | |
95 | nvim = attach('stdio') | |
96 | ||
97 | When the session is not needed anymore, it is recommended to explicitly | |
98 | close it: | |
99 | nvim.close() | |
100 | It is also possible to use the session as a context mangager: | |
101 | with attach('socket', path=thepath) as nvim: | |
102 | print(nvim.funcs.getpid()) | |
103 | print(nvim.current.line) | |
104 | This will automatically close the session when you're done with it, or | |
105 | when an error occured. | |
106 | ||
107 | ||
108 | """ | |
109 | session = (tcp_session(address, port) if session_type == 'tcp' else | |
110 | socket_session(path) if session_type == 'socket' else | |
111 | stdio_session() if session_type == 'stdio' else | |
112 | child_session(argv) if session_type == 'child' else | |
113 | None) | |
114 | ||
115 | if not session: | |
116 | raise Exception('Unknown session type "%s"' % session_type) | |
117 | ||
118 | if decode is None: | |
119 | decode = IS_PYTHON3 | |
120 | ||
121 | return Nvim.from_session(session).with_decode(decode) | |
122 | ||
123 | ||
124 | def setup_logging(name): | |
125 | """Setup logging according to environment variables.""" | |
126 | logger = logging.getLogger(__name__) | |
127 | if 'NVIM_PYTHON_LOG_FILE' in os.environ: | |
128 | prefix = os.environ['NVIM_PYTHON_LOG_FILE'].strip() | |
129 | major_version = sys.version_info[0] | |
130 | logfile = '{}_py{}_{}'.format(prefix, major_version, name) | |
131 | handler = logging.FileHandler(logfile, 'w', 'utf-8') | |
132 | handler.formatter = logging.Formatter( | |
133 | '%(asctime)s [%(levelname)s @ ' | |
134 | '%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s') | |
135 | logging.root.addHandler(handler) | |
136 | level = logging.INFO | |
137 | if 'NVIM_PYTHON_LOG_LEVEL' in os.environ: | |
138 | lvl = getattr(logging, | |
139 | os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(), | |
140 | level) | |
141 | if isinstance(lvl, int): | |
142 | level = lvl | |
143 | logger.setLevel(level) | |
144 | ||
145 | ||
146 | # Required for python 2.6 | |
147 | class NullHandler(logging.Handler): | |
148 | def emit(self, record): | |
149 | pass | |
150 | ||
151 | ||
152 | if not logging.root.handlers: | |
153 | logging.root.addHandler(NullHandler()) |
0 | """Nvim API subpackage. | |
1 | ||
2 | This package implements a higher-level API that wraps msgpack-rpc `Session` | |
3 | instances. | |
4 | """ | |
5 | ||
6 | from .buffer import Buffer | |
7 | from .common import decode_if_bytes, walk | |
8 | from .nvim import Nvim, NvimError | |
9 | from .tabpage import Tabpage | |
10 | from .window import Window | |
11 | ||
12 | ||
13 | __all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError', | |
14 | 'decode_if_bytes', 'walk') |
0 | """API for working with a Nvim Buffer.""" | |
1 | from .common import Remote | |
2 | from ..compat import IS_PYTHON3, check_async | |
3 | ||
4 | ||
5 | __all__ = ('Buffer') | |
6 | ||
7 | ||
8 | if IS_PYTHON3: | |
9 | basestring = str | |
10 | ||
11 | ||
12 | def adjust_index(idx, default=None): | |
13 | """Convert from python indexing convention to nvim indexing convention.""" | |
14 | if idx is None: | |
15 | return default | |
16 | elif idx < 0: | |
17 | return idx - 1 | |
18 | else: | |
19 | return idx | |
20 | ||
21 | ||
22 | class Buffer(Remote): | |
23 | ||
24 | """A remote Nvim buffer.""" | |
25 | ||
26 | _api_prefix = "nvim_buf_" | |
27 | ||
28 | def __len__(self): | |
29 | """Return the number of lines contained in a Buffer.""" | |
30 | return self.request('nvim_buf_line_count') | |
31 | ||
32 | def __getitem__(self, idx): | |
33 | """Get a buffer line or slice by integer index. | |
34 | ||
35 | Indexes may be negative to specify positions from the end of the | |
36 | buffer. For example, -1 is the last line, -2 is the line before that | |
37 | and so on. | |
38 | ||
39 | When retrieving slices, omiting indexes(eg: `buffer[:]`) will bring | |
40 | the whole buffer. | |
41 | """ | |
42 | if not isinstance(idx, slice): | |
43 | i = adjust_index(idx) | |
44 | return self.request('nvim_buf_get_lines', i, i + 1, True)[0] | |
45 | start = adjust_index(idx.start, 0) | |
46 | end = adjust_index(idx.stop, -1) | |
47 | return self.request('nvim_buf_get_lines', start, end, False) | |
48 | ||
49 | def __setitem__(self, idx, item): | |
50 | """Replace a buffer line or slice by integer index. | |
51 | ||
52 | Like with `__getitem__`, indexes may be negative. | |
53 | ||
54 | When replacing slices, omiting indexes(eg: `buffer[:]`) will replace | |
55 | the whole buffer. | |
56 | """ | |
57 | if not isinstance(idx, slice): | |
58 | i = adjust_index(idx) | |
59 | lines = [item] if item is not None else [] | |
60 | return self.request('nvim_buf_set_lines', i, i + 1, True, lines) | |
61 | lines = item if item is not None else [] | |
62 | start = adjust_index(idx.start, 0) | |
63 | end = adjust_index(idx.stop, -1) | |
64 | return self.request('nvim_buf_set_lines', start, end, False, lines) | |
65 | ||
66 | def __iter__(self): | |
67 | """Iterate lines of a buffer. | |
68 | ||
69 | This will retrieve all lines locally before iteration starts. This | |
70 | approach is used because for most cases, the gain is much greater by | |
71 | minimizing the number of API calls by transfering all data needed to | |
72 | work. | |
73 | """ | |
74 | lines = self[:] | |
75 | for line in lines: | |
76 | yield line | |
77 | ||
78 | def __delitem__(self, idx): | |
79 | """Delete line or slice of lines from the buffer. | |
80 | ||
81 | This is the same as __setitem__(idx, []) | |
82 | """ | |
83 | self.__setitem__(idx, None) | |
84 | ||
85 | def append(self, lines, index=-1): | |
86 | """Append a string or list of lines to the buffer.""" | |
87 | if isinstance(lines, (basestring, bytes)): | |
88 | lines = [lines] | |
89 | return self.request('nvim_buf_set_lines', index, index, True, lines) | |
90 | ||
91 | def mark(self, name): | |
92 | """Return (row, col) tuple for a named mark.""" | |
93 | return self.request('nvim_buf_get_mark', name) | |
94 | ||
95 | def range(self, start, end): | |
96 | """Return a `Range` object, which represents part of the Buffer.""" | |
97 | return Range(self, start, end) | |
98 | ||
99 | def add_highlight(self, hl_group, line, col_start=0, | |
100 | col_end=-1, src_id=-1, async_=None, | |
101 | **kwargs): | |
102 | """Add a highlight to the buffer.""" | |
103 | async_ = check_async(async_, kwargs, src_id != 0) | |
104 | return self.request('nvim_buf_add_highlight', src_id, hl_group, | |
105 | line, col_start, col_end, async_=async_) | |
106 | ||
107 | def clear_highlight(self, src_id, line_start=0, line_end=-1, async_=None, | |
108 | **kwargs): | |
109 | """Clear highlights from the buffer.""" | |
110 | async_ = check_async(async_, kwargs, True) | |
111 | self.request('nvim_buf_clear_highlight', src_id, | |
112 | line_start, line_end, async_=async_) | |
113 | ||
114 | def update_highlights(self, src_id, hls, clear_start=0, clear_end=-1, | |
115 | clear=False, async_=True): | |
116 | """Add or update highlights in batch to avoid unnecessary redraws. | |
117 | ||
118 | A `src_id` must have been allocated prior to use of this function. Use | |
119 | for instance `nvim.new_highlight_source()` to get a src_id for your | |
120 | plugin. | |
121 | ||
122 | `hls` should be a list of highlight items. Each item should be a list | |
123 | or tuple on the form `("GroupName", linenr, col_start, col_end)` or | |
124 | `("GroupName", linenr)` to highlight an entire line. | |
125 | ||
126 | By default existing highlights are preserved. Specify a line range with | |
127 | clear_start and clear_end to replace highlights in this range. As a | |
128 | shorthand, use clear=True to clear the entire buffer before adding the | |
129 | new highlights. | |
130 | """ | |
131 | if clear and clear_start is None: | |
132 | clear_start = 0 | |
133 | lua = self._session._get_lua_private() | |
134 | lua.update_highlights(self, src_id, hls, clear_start, clear_end, | |
135 | async_=async_) | |
136 | ||
137 | @property | |
138 | def name(self): | |
139 | """Get the buffer name.""" | |
140 | return self.request('nvim_buf_get_name') | |
141 | ||
142 | @name.setter | |
143 | def name(self, value): | |
144 | """Set the buffer name. BufFilePre/BufFilePost are triggered.""" | |
145 | return self.request('nvim_buf_set_name', value) | |
146 | ||
147 | @property | |
148 | def valid(self): | |
149 | """Return True if the buffer still exists.""" | |
150 | return self.request('nvim_buf_is_valid') | |
151 | ||
152 | @property | |
153 | def number(self): | |
154 | """Get the buffer number.""" | |
155 | return self.handle | |
156 | ||
157 | ||
158 | class Range(object): | |
159 | def __init__(self, buffer, start, end): | |
160 | self._buffer = buffer | |
161 | self.start = start - 1 | |
162 | self.end = end - 1 | |
163 | ||
164 | def __len__(self): | |
165 | return self.end - self.start + 1 | |
166 | ||
167 | def __getitem__(self, idx): | |
168 | if not isinstance(idx, slice): | |
169 | return self._buffer[self._normalize_index(idx)] | |
170 | start = self._normalize_index(idx.start) | |
171 | end = self._normalize_index(idx.stop) | |
172 | if start is None: | |
173 | start = self.start | |
174 | if end is None: | |
175 | end = self.end + 1 | |
176 | return self._buffer[start:end] | |
177 | ||
178 | def __setitem__(self, idx, lines): | |
179 | if not isinstance(idx, slice): | |
180 | self._buffer[self._normalize_index(idx)] = lines | |
181 | return | |
182 | start = self._normalize_index(idx.start) | |
183 | end = self._normalize_index(idx.stop) | |
184 | if start is None: | |
185 | start = self.start | |
186 | if end is None: | |
187 | end = self.end | |
188 | self._buffer[start:end + 1] = lines | |
189 | ||
190 | def __iter__(self): | |
191 | for i in range(self.start, self.end + 1): | |
192 | yield self._buffer[i] | |
193 | ||
194 | def append(self, lines, i=None): | |
195 | i = self._normalize_index(i) | |
196 | if i is None: | |
197 | i = self.end + 1 | |
198 | self._buffer.append(lines, i) | |
199 | ||
200 | def _normalize_index(self, index): | |
201 | if index is None: | |
202 | return None | |
203 | if index < 0: | |
204 | index = self.end | |
205 | else: | |
206 | index += self.start | |
207 | if index > self.end: | |
208 | index = self.end | |
209 | return index |
0 | """Code shared between the API classes.""" | |
1 | import functools | |
2 | ||
3 | from msgpack import unpackb | |
4 | ||
5 | from ..compat import unicode_errors_default | |
6 | ||
7 | ||
8 | class Remote(object): | |
9 | ||
10 | """Base class for Nvim objects(buffer/window/tabpage). | |
11 | ||
12 | Each type of object has it's own specialized class with API wrappers around | |
13 | the msgpack-rpc session. This implements equality which takes the remote | |
14 | object handle into consideration. | |
15 | """ | |
16 | ||
17 | def __init__(self, session, code_data): | |
18 | """Initialize from session and code_data immutable object. | |
19 | ||
20 | The `code_data` contains serialization information required for | |
21 | msgpack-rpc calls. It must be immutable for Buffer equality to work. | |
22 | """ | |
23 | self._session = session | |
24 | self.code_data = code_data | |
25 | self.handle = unpackb(code_data[1]) | |
26 | self.api = RemoteApi(self, self._api_prefix) | |
27 | self.vars = RemoteMap(self, self._api_prefix + 'get_var', | |
28 | self._api_prefix + 'set_var') | |
29 | self.options = RemoteMap(self, self._api_prefix + 'get_option', | |
30 | self._api_prefix + 'set_option') | |
31 | ||
32 | def __repr__(self): | |
33 | """Get text representation of the object.""" | |
34 | return '<%s(handle=%r)>' % ( | |
35 | self.__class__.__name__, | |
36 | self.handle, | |
37 | ) | |
38 | ||
39 | def __eq__(self, other): | |
40 | """Return True if `self` and `other` are the same object.""" | |
41 | return (hasattr(other, 'code_data') | |
42 | and other.code_data == self.code_data) | |
43 | ||
44 | def __hash__(self): | |
45 | """Return hash based on remote object id.""" | |
46 | return self.code_data.__hash__() | |
47 | ||
48 | def request(self, name, *args, **kwargs): | |
49 | """Wrapper for nvim.request.""" | |
50 | return self._session.request(name, self, *args, **kwargs) | |
51 | ||
52 | ||
53 | class RemoteApi(object): | |
54 | ||
55 | """Wrapper to allow api methods to be called like python methods.""" | |
56 | ||
57 | def __init__(self, obj, api_prefix): | |
58 | """Initialize a RemoteApi with object and api prefix.""" | |
59 | self._obj = obj | |
60 | self._api_prefix = api_prefix | |
61 | ||
62 | def __getattr__(self, name): | |
63 | """Return wrapper to named api method.""" | |
64 | return functools.partial(self._obj.request, self._api_prefix + name) | |
65 | ||
66 | ||
67 | class RemoteMap(object): | |
68 | ||
69 | """Represents a string->object map stored in Nvim. | |
70 | ||
71 | This is the dict counterpart to the `RemoteSequence` class, but it is used | |
72 | as a generic way of retrieving values from the various map-like data | |
73 | structures present in Nvim. | |
74 | ||
75 | It is used to provide a dict-like API to vim variables and options. | |
76 | """ | |
77 | ||
78 | def __init__(self, obj, get_method, set_method=None): | |
79 | """Initialize a RemoteMap with session, getter/setter.""" | |
80 | self._get = functools.partial(obj.request, get_method) | |
81 | self._set = None | |
82 | if set_method: | |
83 | self._set = functools.partial(obj.request, set_method) | |
84 | ||
85 | def __getitem__(self, key): | |
86 | """Return a map value by key.""" | |
87 | return self._get(key) | |
88 | ||
89 | def __setitem__(self, key, value): | |
90 | """Set a map value by key(if the setter was provided).""" | |
91 | if not self._set: | |
92 | raise TypeError('This dict is read-only') | |
93 | self._set(key, value) | |
94 | ||
95 | def __delitem__(self, key): | |
96 | """Delete a map value by associating None with the key.""" | |
97 | if not self._set: | |
98 | raise TypeError('This dict is read-only') | |
99 | return self._set(key, None) | |
100 | ||
101 | def __contains__(self, key): | |
102 | """Check if key is present in the map.""" | |
103 | try: | |
104 | self._get(key) | |
105 | return True | |
106 | except Exception: | |
107 | return False | |
108 | ||
109 | def get(self, key, default=None): | |
110 | """Return value for key if present, else a default value.""" | |
111 | try: | |
112 | return self._get(key) | |
113 | except Exception: | |
114 | return default | |
115 | ||
116 | ||
117 | class RemoteSequence(object): | |
118 | ||
119 | """Represents a sequence of objects stored in Nvim. | |
120 | ||
121 | This class is used to wrap msgapck-rpc functions that work on Nvim | |
122 | sequences(of lines, buffers, windows and tabpages) with an API that | |
123 | is similar to the one provided by the python-vim interface. | |
124 | ||
125 | For example, the 'windows' property of the `Nvim` class is a RemoteSequence | |
126 | sequence instance, and the expression `nvim.windows[0]` is translated to | |
127 | session.request('nvim_list_wins')[0]. | |
128 | ||
129 | One important detail about this class is that all methods will fetch the | |
130 | sequence into a list and perform the necessary manipulation | |
131 | locally(iteration, indexing, counting, etc). | |
132 | """ | |
133 | ||
134 | def __init__(self, session, method): | |
135 | """Initialize a RemoteSequence with session, method.""" | |
136 | self._fetch = functools.partial(session.request, method) | |
137 | ||
138 | def __len__(self): | |
139 | """Return the length of the remote sequence.""" | |
140 | return len(self._fetch()) | |
141 | ||
142 | def __getitem__(self, idx): | |
143 | """Return a sequence item by index.""" | |
144 | if not isinstance(idx, slice): | |
145 | return self._fetch()[idx] | |
146 | return self._fetch()[idx.start:idx.stop] | |
147 | ||
148 | def __iter__(self): | |
149 | """Return an iterator for the sequence.""" | |
150 | items = self._fetch() | |
151 | for item in items: | |
152 | yield item | |
153 | ||
154 | def __contains__(self, item): | |
155 | """Check if an item is present in the sequence.""" | |
156 | return item in self._fetch() | |
157 | ||
158 | ||
159 | def _identity(obj, session, method, kind): | |
160 | return obj | |
161 | ||
162 | ||
163 | def decode_if_bytes(obj, mode=True): | |
164 | """Decode obj if it is bytes.""" | |
165 | if mode is True: | |
166 | mode = unicode_errors_default | |
167 | if isinstance(obj, bytes): | |
168 | return obj.decode("utf-8", errors=mode) | |
169 | return obj | |
170 | ||
171 | ||
172 | def walk(fn, obj, *args, **kwargs): | |
173 | """Recursively walk an object graph applying `fn`/`args` to objects.""" | |
174 | if type(obj) in [list, tuple]: | |
175 | return list(walk(fn, o, *args) for o in obj) | |
176 | if type(obj) is dict: | |
177 | return dict((walk(fn, k, *args), walk(fn, v, *args)) for k, v in | |
178 | obj.items()) | |
179 | return fn(obj, *args, **kwargs) |
0 | """Main Nvim interface.""" | |
1 | import os | |
2 | import sys | |
3 | import threading | |
4 | from functools import partial | |
5 | from traceback import format_stack | |
6 | ||
7 | from msgpack import ExtType | |
8 | ||
9 | from .buffer import Buffer | |
10 | from .common import (Remote, RemoteApi, RemoteMap, RemoteSequence, | |
11 | decode_if_bytes, walk) | |
12 | from .tabpage import Tabpage | |
13 | from .window import Window | |
14 | from ..compat import IS_PYTHON3 | |
15 | from ..util import Version, format_exc_skip | |
16 | ||
17 | __all__ = ('Nvim') | |
18 | ||
19 | ||
20 | os_chdir = os.chdir | |
21 | ||
22 | lua_module = """ | |
23 | local a = vim.api | |
24 | local function update_highlights(buf, src_id, hls, clear_first, clear_end) | |
25 | if clear_first ~= nil then | |
26 | a.nvim_buf_clear_highlight(buf, src_id, clear_first, clear_end) | |
27 | end | |
28 | for _,hl in pairs(hls) do | |
29 | local group, line, col_start, col_end = unpack(hl) | |
30 | if col_start == nil then | |
31 | col_start = 0 | |
32 | end | |
33 | if col_end == nil then | |
34 | col_end = -1 | |
35 | end | |
36 | a.nvim_buf_add_highlight(buf, src_id, group, line, col_start, col_end) | |
37 | end | |
38 | end | |
39 | ||
40 | local chid = ... | |
41 | local mod = {update_highlights=update_highlights} | |
42 | _G["_pynvim_"..chid] = mod | |
43 | """ | |
44 | ||
45 | ||
46 | class Nvim(object): | |
47 | ||
48 | """Class that represents a remote Nvim instance. | |
49 | ||
50 | This class is main entry point to Nvim remote API, it is a wrapper | |
51 | around Session instances. | |
52 | ||
53 | The constructor of this class must not be called directly. Instead, the | |
54 | `from_session` class method should be used to create the first instance | |
55 | from a raw `Session` instance. | |
56 | ||
57 | Subsequent instances for the same session can be created by calling the | |
58 | `with_decode` instance method to change the decoding behavior or | |
59 | `SubClass.from_nvim(nvim)` where `SubClass` is a subclass of `Nvim`, which | |
60 | is useful for having multiple `Nvim` objects that behave differently | |
61 | without one affecting the other. | |
62 | ||
63 | When this library is used on python3.4+, asyncio event loop is guaranteed | |
64 | to be used. It is available as the "loop" attribute of this class. Note | |
65 | that asyncio callbacks cannot make blocking requests, which includes | |
66 | accessing state-dependent attributes. They should instead schedule another | |
67 | callback using nvim.async_call, which will not have this restriction. | |
68 | """ | |
69 | ||
70 | @classmethod | |
71 | def from_session(cls, session): | |
72 | """Create a new Nvim instance for a Session instance. | |
73 | ||
74 | This method must be called to create the first Nvim instance, since it | |
75 | queries Nvim metadata for type information and sets a SessionHook for | |
76 | creating specialized objects from Nvim remote handles. | |
77 | """ | |
78 | session.error_wrapper = lambda e: NvimError(e[1]) | |
79 | channel_id, metadata = session.request(b'vim_get_api_info') | |
80 | ||
81 | if IS_PYTHON3: | |
82 | # decode all metadata strings for python3 | |
83 | metadata = walk(decode_if_bytes, metadata) | |
84 | ||
85 | types = { | |
86 | metadata['types']['Buffer']['id']: Buffer, | |
87 | metadata['types']['Window']['id']: Window, | |
88 | metadata['types']['Tabpage']['id']: Tabpage, | |
89 | } | |
90 | ||
91 | return cls(session, channel_id, metadata, types) | |
92 | ||
93 | @classmethod | |
94 | def from_nvim(cls, nvim): | |
95 | """Create a new Nvim instance from an existing instance.""" | |
96 | return cls(nvim._session, nvim.channel_id, nvim.metadata, | |
97 | nvim.types, nvim._decode, nvim._err_cb) | |
98 | ||
99 | def __init__(self, session, channel_id, metadata, types, | |
100 | decode=False, err_cb=None): | |
101 | """Initialize a new Nvim instance. This method is module-private.""" | |
102 | self._session = session | |
103 | self.channel_id = channel_id | |
104 | self.metadata = metadata | |
105 | version = metadata.get("version", {"api_level": 0}) | |
106 | self.version = Version(**version) | |
107 | self.types = types | |
108 | self.api = RemoteApi(self, 'nvim_') | |
109 | self.vars = RemoteMap(self, 'nvim_get_var', 'nvim_set_var') | |
110 | self.vvars = RemoteMap(self, 'nvim_get_vvar', None) | |
111 | self.options = RemoteMap(self, 'nvim_get_option', 'nvim_set_option') | |
112 | self.buffers = Buffers(self) | |
113 | self.windows = RemoteSequence(self, 'nvim_list_wins') | |
114 | self.tabpages = RemoteSequence(self, 'nvim_list_tabpages') | |
115 | self.current = Current(self) | |
116 | self.session = CompatibilitySession(self) | |
117 | self.funcs = Funcs(self) | |
118 | self.lua = LuaFuncs(self) | |
119 | self.error = NvimError | |
120 | self._decode = decode | |
121 | self._err_cb = err_cb | |
122 | ||
123 | # only on python3.4+ we expose asyncio | |
124 | if IS_PYTHON3: | |
125 | self.loop = self._session.loop._loop | |
126 | ||
127 | def _from_nvim(self, obj, decode=None): | |
128 | if decode is None: | |
129 | decode = self._decode | |
130 | if type(obj) is ExtType: | |
131 | cls = self.types[obj.code] | |
132 | return cls(self, (obj.code, obj.data)) | |
133 | if decode: | |
134 | obj = decode_if_bytes(obj, decode) | |
135 | return obj | |
136 | ||
137 | def _to_nvim(self, obj): | |
138 | if isinstance(obj, Remote): | |
139 | return ExtType(*obj.code_data) | |
140 | return obj | |
141 | ||
142 | def _get_lua_private(self): | |
143 | if not getattr(self._session, "_has_lua", False): | |
144 | self.exec_lua(lua_module, self.channel_id) | |
145 | self._session._has_lua = True | |
146 | return getattr(self.lua, "_pynvim_{}".format(self.channel_id)) | |
147 | ||
148 | def request(self, name, *args, **kwargs): | |
149 | r"""Send an API request or notification to nvim. | |
150 | ||
151 | It is rarely needed to call this function directly, as most API | |
152 | functions have python wrapper functions. The `api` object can | |
153 | be also be used to call API functions as methods: | |
154 | ||
155 | vim.api.err_write('ERROR\n', async_=True) | |
156 | vim.current.buffer.api.get_mark('.') | |
157 | ||
158 | is equivalent to | |
159 | ||
160 | vim.request('nvim_err_write', 'ERROR\n', async_=True) | |
161 | vim.request('nvim_buf_get_mark', vim.current.buffer, '.') | |
162 | ||
163 | ||
164 | Normally a blocking request will be sent. If the `async_` flag is | |
165 | present and True, a asynchronous notification is sent instead. This | |
166 | will never block, and the return value or error is ignored. | |
167 | """ | |
168 | if (self._session._loop_thread is not None | |
169 | and threading.current_thread() != self._session._loop_thread): | |
170 | ||
171 | msg = ("Request from non-main thread.\n" | |
172 | "Requests from different threads should be wrapped " | |
173 | "with nvim.async_call(cb, ...) \n{}\n" | |
174 | .format('\n'.join(format_stack(None, 5)[:-1]))) | |
175 | ||
176 | self.async_call(self._err_cb, msg) | |
177 | raise NvimError("request from non-main thread") | |
178 | ||
179 | decode = kwargs.pop('decode', self._decode) | |
180 | args = walk(self._to_nvim, args) | |
181 | res = self._session.request(name, *args, **kwargs) | |
182 | return walk(self._from_nvim, res, decode=decode) | |
183 | ||
184 | def next_message(self): | |
185 | """Block until a message(request or notification) is available. | |
186 | ||
187 | If any messages were previously enqueued, return the first in queue. | |
188 | If not, run the event loop until one is received. | |
189 | """ | |
190 | msg = self._session.next_message() | |
191 | if msg: | |
192 | return walk(self._from_nvim, msg) | |
193 | ||
194 | def run_loop(self, request_cb, notification_cb, | |
195 | setup_cb=None, err_cb=None): | |
196 | """Run the event loop to receive requests and notifications from Nvim. | |
197 | ||
198 | This should not be called from a plugin running in the host, which | |
199 | already runs the loop and dispatches events to plugins. | |
200 | """ | |
201 | if err_cb is None: | |
202 | err_cb = sys.stderr.write | |
203 | self._err_cb = err_cb | |
204 | ||
205 | def filter_request_cb(name, args): | |
206 | name = self._from_nvim(name) | |
207 | args = walk(self._from_nvim, args) | |
208 | try: | |
209 | result = request_cb(name, args) | |
210 | except Exception: | |
211 | msg = ("error caught in request handler '{} {}'\n{}\n\n" | |
212 | .format(name, args, format_exc_skip(1))) | |
213 | self._err_cb(msg) | |
214 | raise | |
215 | return walk(self._to_nvim, result) | |
216 | ||
217 | def filter_notification_cb(name, args): | |
218 | name = self._from_nvim(name) | |
219 | args = walk(self._from_nvim, args) | |
220 | try: | |
221 | notification_cb(name, args) | |
222 | except Exception: | |
223 | msg = ("error caught in notification handler '{} {}'\n{}\n\n" | |
224 | .format(name, args, format_exc_skip(1))) | |
225 | self._err_cb(msg) | |
226 | raise | |
227 | ||
228 | self._session.run(filter_request_cb, filter_notification_cb, setup_cb) | |
229 | ||
230 | def stop_loop(self): | |
231 | """Stop the event loop being started with `run_loop`.""" | |
232 | self._session.stop() | |
233 | ||
234 | def close(self): | |
235 | """Close the nvim session and release its resources.""" | |
236 | self._session.close() | |
237 | ||
238 | def __enter__(self): | |
239 | """Enter nvim session as a context manager.""" | |
240 | return self | |
241 | ||
242 | def __exit__(self, *exc_info): | |
243 | """Exit nvim session as a context manager. | |
244 | ||
245 | Closes the event loop. | |
246 | """ | |
247 | self.close() | |
248 | ||
249 | def with_decode(self, decode=True): | |
250 | """Initialize a new Nvim instance.""" | |
251 | return Nvim(self._session, self.channel_id, | |
252 | self.metadata, self.types, decode, self._err_cb) | |
253 | ||
254 | def ui_attach(self, width, height, rgb=None, **kwargs): | |
255 | """Register as a remote UI. | |
256 | ||
257 | After this method is called, the client will receive redraw | |
258 | notifications. | |
259 | """ | |
260 | options = kwargs | |
261 | if rgb is not None: | |
262 | options['rgb'] = rgb | |
263 | return self.request('nvim_ui_attach', width, height, options) | |
264 | ||
265 | def ui_detach(self): | |
266 | """Unregister as a remote UI.""" | |
267 | return self.request('nvim_ui_detach') | |
268 | ||
269 | def ui_try_resize(self, width, height): | |
270 | """Notify nvim that the client window has resized. | |
271 | ||
272 | If possible, nvim will send a redraw request to resize. | |
273 | """ | |
274 | return self.request('ui_try_resize', width, height) | |
275 | ||
276 | def subscribe(self, event): | |
277 | """Subscribe to a Nvim event.""" | |
278 | return self.request('nvim_subscribe', event) | |
279 | ||
280 | def unsubscribe(self, event): | |
281 | """Unsubscribe to a Nvim event.""" | |
282 | return self.request('nvim_unsubscribe', event) | |
283 | ||
284 | def command(self, string, **kwargs): | |
285 | """Execute a single ex command.""" | |
286 | return self.request('nvim_command', string, **kwargs) | |
287 | ||
288 | def command_output(self, string): | |
289 | """Execute a single ex command and return the output.""" | |
290 | return self.request('nvim_command_output', string) | |
291 | ||
292 | def eval(self, string, **kwargs): | |
293 | """Evaluate a vimscript expression.""" | |
294 | return self.request('nvim_eval', string, **kwargs) | |
295 | ||
296 | def call(self, name, *args, **kwargs): | |
297 | """Call a vimscript function.""" | |
298 | return self.request('nvim_call_function', name, args, **kwargs) | |
299 | ||
300 | def exec_lua(self, code, *args, **kwargs): | |
301 | """Execute lua code. | |
302 | ||
303 | Additional parameters are available as `...` inside the lua chunk. | |
304 | Only statements are executed. To evaluate an expression, prefix it | |
305 | with `return`: `return my_function(...)` | |
306 | ||
307 | There is a shorthand syntax to call lua functions with arguments: | |
308 | ||
309 | nvim.lua.func(1,2) | |
310 | nvim.lua.mymod.myfunction(data, async_=True) | |
311 | ||
312 | is equivalent to | |
313 | ||
314 | nvim.exec_lua("return func(...)", 1, 2) | |
315 | nvim.exec_lua("mymod.myfunction(...)", data, async_=True) | |
316 | ||
317 | Note that with `async_=True` there is no return value. | |
318 | """ | |
319 | return self.request('nvim_execute_lua', code, args, **kwargs) | |
320 | ||
321 | def strwidth(self, string): | |
322 | """Return the number of display cells `string` occupies. | |
323 | ||
324 | Tab is counted as one cell. | |
325 | """ | |
326 | return self.request('nvim_strwidth', string) | |
327 | ||
328 | def list_runtime_paths(self): | |
329 | """Return a list of paths contained in the 'runtimepath' option.""" | |
330 | return self.request('nvim_list_runtime_paths') | |
331 | ||
332 | def foreach_rtp(self, cb): | |
333 | """Invoke `cb` for each path in 'runtimepath'. | |
334 | ||
335 | Call the given callable for each path in 'runtimepath' until either | |
336 | callable returns something but None, the exception is raised or there | |
337 | are no longer paths. If stopped in case callable returned non-None, | |
338 | vim.foreach_rtp function returns the value returned by callable. | |
339 | """ | |
340 | for path in self.request('nvim_list_runtime_paths'): | |
341 | try: | |
342 | if cb(path) is not None: | |
343 | break | |
344 | except Exception: | |
345 | break | |
346 | ||
347 | def chdir(self, dir_path): | |
348 | """Run os.chdir, then all appropriate vim stuff.""" | |
349 | os_chdir(dir_path) | |
350 | return self.request('nvim_set_current_dir', dir_path) | |
351 | ||
352 | def feedkeys(self, keys, options='', escape_csi=True): | |
353 | """Push `keys` to Nvim user input buffer. | |
354 | ||
355 | Options can be a string with the following character flags: | |
356 | - 'm': Remap keys. This is default. | |
357 | - 'n': Do not remap keys. | |
358 | - 't': Handle keys as if typed; otherwise they are handled as if coming | |
359 | from a mapping. This matters for undo, opening folds, etc. | |
360 | """ | |
361 | return self.request('nvim_feedkeys', keys, options, escape_csi) | |
362 | ||
363 | def input(self, bytes): | |
364 | """Push `bytes` to Nvim low level input buffer. | |
365 | ||
366 | Unlike `feedkeys()`, this uses the lowest level input buffer and the | |
367 | call is not deferred. It returns the number of bytes actually | |
368 | written(which can be less than what was requested if the buffer is | |
369 | full). | |
370 | """ | |
371 | return self.request('nvim_input', bytes) | |
372 | ||
373 | def replace_termcodes(self, string, from_part=False, do_lt=True, | |
374 | special=True): | |
375 | r"""Replace any terminal code strings by byte sequences. | |
376 | ||
377 | The returned sequences are Nvim's internal representation of keys, | |
378 | for example: | |
379 | ||
380 | <esc> -> '\x1b' | |
381 | <cr> -> '\r' | |
382 | <c-l> -> '\x0c' | |
383 | <up> -> '\x80ku' | |
384 | ||
385 | The returned sequences can be used as input to `feedkeys`. | |
386 | """ | |
387 | return self.request('nvim_replace_termcodes', string, | |
388 | from_part, do_lt, special) | |
389 | ||
390 | def out_write(self, msg, **kwargs): | |
391 | """Print `msg` as a normal message.""" | |
392 | return self.request('nvim_out_write', msg, **kwargs) | |
393 | ||
394 | def err_write(self, msg, **kwargs): | |
395 | """Print `msg` as an error message.""" | |
396 | if self._thread_invalid(): | |
397 | # special case: if a non-main thread writes to stderr | |
398 | # i.e. due to an uncaught exception, pass it through | |
399 | # without raising an additional exception. | |
400 | self.async_call(self.err_write, msg, **kwargs) | |
401 | return | |
402 | return self.request('nvim_err_write', msg, **kwargs) | |
403 | ||
404 | def _thread_invalid(self): | |
405 | return (self._session._loop_thread is not None | |
406 | and threading.current_thread() != self._session._loop_thread) | |
407 | ||
408 | def quit(self, quit_command='qa!'): | |
409 | """Send a quit command to Nvim. | |
410 | ||
411 | By default, the quit command is 'qa!' which will make Nvim quit without | |
412 | saving anything. | |
413 | """ | |
414 | try: | |
415 | self.command(quit_command) | |
416 | except IOError: | |
417 | # sending a quit command will raise an IOError because the | |
418 | # connection is closed before a response is received. Safe to | |
419 | # ignore it. | |
420 | pass | |
421 | ||
422 | def new_highlight_source(self): | |
423 | """Return new src_id for use with Buffer.add_highlight.""" | |
424 | return self.current.buffer.add_highlight("", 0, src_id=0) | |
425 | ||
426 | def async_call(self, fn, *args, **kwargs): | |
427 | """Schedule `fn` to be called by the event loop soon. | |
428 | ||
429 | This function is thread-safe, and is the only way code not | |
430 | on the main thread could interact with nvim api objects. | |
431 | ||
432 | This function can also be called in a synchronous | |
433 | event handler, just before it returns, to defer execution | |
434 | that shouldn't block neovim. | |
435 | """ | |
436 | call_point = ''.join(format_stack(None, 5)[:-1]) | |
437 | ||
438 | def handler(): | |
439 | try: | |
440 | fn(*args, **kwargs) | |
441 | except Exception as err: | |
442 | msg = ("error caught while executing async callback:\n" | |
443 | "{!r}\n{}\n \nthe call was requested at\n{}" | |
444 | .format(err, format_exc_skip(1), call_point)) | |
445 | self._err_cb(msg) | |
446 | raise | |
447 | self._session.threadsafe_call(handler) | |
448 | ||
449 | ||
450 | class Buffers(object): | |
451 | ||
452 | """Remote NVim buffers. | |
453 | ||
454 | Currently the interface for interacting with remote NVim buffers is the | |
455 | `nvim_list_bufs` msgpack-rpc function. Most methods fetch the list of | |
456 | buffers from NVim. | |
457 | ||
458 | Conforms to *python-buffers*. | |
459 | """ | |
460 | ||
461 | def __init__(self, nvim): | |
462 | """Initialize a Buffers object with Nvim object `nvim`.""" | |
463 | self._fetch_buffers = nvim.api.list_bufs | |
464 | ||
465 | def __len__(self): | |
466 | """Return the count of buffers.""" | |
467 | return len(self._fetch_buffers()) | |
468 | ||
469 | def __getitem__(self, number): | |
470 | """Return the Buffer object matching buffer number `number`.""" | |
471 | for b in self._fetch_buffers(): | |
472 | if b.number == number: | |
473 | return b | |
474 | raise KeyError(number) | |
475 | ||
476 | def __contains__(self, b): | |
477 | """Return whether Buffer `b` is a known valid buffer.""" | |
478 | return isinstance(b, Buffer) and b.valid | |
479 | ||
480 | def __iter__(self): | |
481 | """Return an iterator over the list of buffers.""" | |
482 | return iter(self._fetch_buffers()) | |
483 | ||
484 | ||
485 | class CompatibilitySession(object): | |
486 | ||
487 | """Helper class for API compatibility.""" | |
488 | ||
489 | def __init__(self, nvim): | |
490 | self.threadsafe_call = nvim.async_call | |
491 | ||
492 | ||
493 | class Current(object): | |
494 | ||
495 | """Helper class for emulating vim.current from python-vim.""" | |
496 | ||
497 | def __init__(self, session): | |
498 | self._session = session | |
499 | self.range = None | |
500 | ||
501 | @property | |
502 | def line(self): | |
503 | return self._session.request('nvim_get_current_line') | |
504 | ||
505 | @line.setter | |
506 | def line(self, line): | |
507 | return self._session.request('nvim_set_current_line', line) | |
508 | ||
509 | @line.deleter | |
510 | def line(self): | |
511 | return self._session.request('nvim_del_current_line') | |
512 | ||
513 | @property | |
514 | def buffer(self): | |
515 | return self._session.request('nvim_get_current_buf') | |
516 | ||
517 | @buffer.setter | |
518 | def buffer(self, buffer): | |
519 | return self._session.request('nvim_set_current_buf', buffer) | |
520 | ||
521 | @property | |
522 | def window(self): | |
523 | return self._session.request('nvim_get_current_win') | |
524 | ||
525 | @window.setter | |
526 | def window(self, window): | |
527 | return self._session.request('nvim_set_current_win', window) | |
528 | ||
529 | @property | |
530 | def tabpage(self): | |
531 | return self._session.request('nvim_get_current_tabpage') | |
532 | ||
533 | @tabpage.setter | |
534 | def tabpage(self, tabpage): | |
535 | return self._session.request('nvim_set_current_tabpage', tabpage) | |
536 | ||
537 | ||
538 | class Funcs(object): | |
539 | ||
540 | """Helper class for functional vimscript interface.""" | |
541 | ||
542 | def __init__(self, nvim): | |
543 | self._nvim = nvim | |
544 | ||
545 | def __getattr__(self, name): | |
546 | return partial(self._nvim.call, name) | |
547 | ||
548 | ||
549 | class LuaFuncs(object): | |
550 | ||
551 | """Wrapper to allow lua functions to be called like python methods.""" | |
552 | ||
553 | def __init__(self, nvim, name=""): | |
554 | self._nvim = nvim | |
555 | self.name = name | |
556 | ||
557 | def __getattr__(self, name): | |
558 | """Return wrapper to named api method.""" | |
559 | prefix = self.name + "." if self.name else "" | |
560 | return LuaFuncs(self._nvim, prefix + name) | |
561 | ||
562 | def __call__(self, *args, **kwargs): | |
563 | # first new function after keyword rename, be a bit noisy | |
564 | if 'async' in kwargs: | |
565 | raise ValueError('"async" argument is not allowed. ' | |
566 | 'Use "async_" instead.') | |
567 | async_ = kwargs.get('async_', False) | |
568 | pattern = "return {}(...)" if not async_ else "{}(...)" | |
569 | code = pattern.format(self.name) | |
570 | return self._nvim.exec_lua(code, *args, **kwargs) | |
571 | ||
572 | ||
573 | class NvimError(Exception): | |
574 | pass |
0 | """API for working with Nvim tabpages.""" | |
1 | from .common import Remote, RemoteSequence | |
2 | ||
3 | ||
4 | __all__ = ('Tabpage') | |
5 | ||
6 | ||
7 | class Tabpage(Remote): | |
8 | """A remote Nvim tabpage.""" | |
9 | ||
10 | _api_prefix = "nvim_tabpage_" | |
11 | ||
12 | def __init__(self, *args): | |
13 | """Initialize from session and code_data immutable object. | |
14 | ||
15 | The `code_data` contains serialization information required for | |
16 | msgpack-rpc calls. It must be immutable for Buffer equality to work. | |
17 | """ | |
18 | super(Tabpage, self).__init__(*args) | |
19 | self.windows = RemoteSequence(self, 'nvim_tabpage_list_wins') | |
20 | ||
21 | @property | |
22 | def window(self): | |
23 | """Get the `Window` currently focused on the tabpage.""" | |
24 | return self.request('nvim_tabpage_get_win') | |
25 | ||
26 | @property | |
27 | def valid(self): | |
28 | """Return True if the tabpage still exists.""" | |
29 | return self.request('nvim_tabpage_is_valid') | |
30 | ||
31 | @property | |
32 | def number(self): | |
33 | """Get the tabpage number.""" | |
34 | return self.request('nvim_tabpage_get_number') |
0 | """API for working with Nvim windows.""" | |
1 | from .common import Remote | |
2 | ||
3 | ||
4 | __all__ = ('Window') | |
5 | ||
6 | ||
7 | class Window(Remote): | |
8 | ||
9 | """A remote Nvim window.""" | |
10 | ||
11 | _api_prefix = "nvim_win_" | |
12 | ||
13 | @property | |
14 | def buffer(self): | |
15 | """Get the `Buffer` currently being displayed by the window.""" | |
16 | return self.request('nvim_win_get_buf') | |
17 | ||
18 | @property | |
19 | def cursor(self): | |
20 | """Get the (row, col) tuple with the current cursor position.""" | |
21 | return self.request('nvim_win_get_cursor') | |
22 | ||
23 | @cursor.setter | |
24 | def cursor(self, pos): | |
25 | """Set the (row, col) tuple as the new cursor position.""" | |
26 | return self.request('nvim_win_set_cursor', pos) | |
27 | ||
28 | @property | |
29 | def height(self): | |
30 | """Get the window height in rows.""" | |
31 | return self.request('nvim_win_get_height') | |
32 | ||
33 | @height.setter | |
34 | def height(self, height): | |
35 | """Set the window height in rows.""" | |
36 | return self.request('nvim_win_set_height', height) | |
37 | ||
38 | @property | |
39 | def width(self): | |
40 | """Get the window width in rows.""" | |
41 | return self.request('nvim_win_get_width') | |
42 | ||
43 | @width.setter | |
44 | def width(self, width): | |
45 | """Set the window height in rows.""" | |
46 | return self.request('nvim_win_set_width', width) | |
47 | ||
48 | @property | |
49 | def row(self): | |
50 | """0-indexed, on-screen window position(row) in display cells.""" | |
51 | return self.request('nvim_win_get_position')[0] | |
52 | ||
53 | @property | |
54 | def col(self): | |
55 | """0-indexed, on-screen window position(col) in display cells.""" | |
56 | return self.request('nvim_win_get_position')[1] | |
57 | ||
58 | @property | |
59 | def tabpage(self): | |
60 | """Get the `Tabpage` that contains the window.""" | |
61 | return self.request('nvim_win_get_tabpage') | |
62 | ||
63 | @property | |
64 | def valid(self): | |
65 | """Return True if the window still exists.""" | |
66 | return self.request('nvim_win_is_valid') | |
67 | ||
68 | @property | |
69 | def number(self): | |
70 | """Get the window number.""" | |
71 | return self.request('nvim_win_get_number') |
0 | """Code for compatibility across Python versions.""" | |
1 | ||
2 | import sys | |
3 | import warnings | |
4 | from imp import find_module as original_find_module | |
5 | ||
6 | ||
7 | IS_PYTHON3 = sys.version_info >= (3, 0) | |
8 | ||
9 | ||
10 | if IS_PYTHON3: | |
11 | def find_module(fullname, path): | |
12 | """Compatibility wrapper for imp.find_module. | |
13 | ||
14 | Automatically decodes arguments of find_module, in Python3 | |
15 | they must be Unicode | |
16 | """ | |
17 | if isinstance(fullname, bytes): | |
18 | fullname = fullname.decode() | |
19 | if isinstance(path, bytes): | |
20 | path = path.decode() | |
21 | elif isinstance(path, list): | |
22 | newpath = [] | |
23 | for element in path: | |
24 | if isinstance(element, bytes): | |
25 | newpath.append(element.decode()) | |
26 | else: | |
27 | newpath.append(element) | |
28 | path = newpath | |
29 | return original_find_module(fullname, path) | |
30 | ||
31 | # There is no 'long' type in Python3 just int | |
32 | long = int | |
33 | unicode_errors_default = 'surrogateescape' | |
34 | else: | |
35 | find_module = original_find_module | |
36 | unicode_errors_default = 'strict' | |
37 | ||
38 | NUM_TYPES = (int, long, float) | |
39 | ||
40 | ||
41 | def check_async(async_, kwargs, default): | |
42 | """Return a value of 'async' in kwargs or default when async_ is None. | |
43 | ||
44 | This helper function exists for backward compatibility (See #274). | |
45 | It shows a warning message when 'async' in kwargs is used to note users. | |
46 | """ | |
47 | if async_ is not None: | |
48 | return async_ | |
49 | elif 'async' in kwargs: | |
50 | warnings.warn( | |
51 | '"async" attribute is deprecated. Use "async_" instead.', | |
52 | DeprecationWarning, | |
53 | ) | |
54 | return kwargs.pop('async') | |
55 | else: | |
56 | return default |
0 | """Msgpack-rpc subpackage. | |
1 | ||
2 | This package implements a msgpack-rpc client. While it was designed for | |
3 | handling some Nvim particularities(server->client requests for example), the | |
4 | code here should work with other msgpack-rpc servers. | |
5 | """ | |
6 | from .async_session import AsyncSession | |
7 | from .event_loop import EventLoop | |
8 | from .msgpack_stream import MsgpackStream | |
9 | from .session import ErrorResponse, Session | |
10 | ||
11 | ||
12 | __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session', | |
13 | 'ErrorResponse') | |
14 | ||
15 | ||
16 | def session(transport_type='stdio', *args, **kwargs): | |
17 | loop = EventLoop(transport_type, *args, **kwargs) | |
18 | msgpack_stream = MsgpackStream(loop) | |
19 | async_session = AsyncSession(msgpack_stream) | |
20 | session = Session(async_session) | |
21 | return session | |
22 | ||
23 | ||
24 | def tcp_session(address, port=7450): | |
25 | """Create a msgpack-rpc session from a tcp address/port.""" | |
26 | return session('tcp', address, port) | |
27 | ||
28 | ||
29 | def socket_session(path): | |
30 | """Create a msgpack-rpc session from a unix domain socket.""" | |
31 | return session('socket', path) | |
32 | ||
33 | ||
34 | def stdio_session(): | |
35 | """Create a msgpack-rpc session from stdin/stdout.""" | |
36 | return session('stdio') | |
37 | ||
38 | ||
39 | def child_session(argv): | |
40 | """Create a msgpack-rpc session from a new Nvim instance.""" | |
41 | return session('child', argv) |
0 | """Asynchronous msgpack-rpc handling in the event loop pipeline.""" | |
1 | import logging | |
2 | from traceback import format_exc | |
3 | ||
4 | ||
5 | logger = logging.getLogger(__name__) | |
6 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
7 | ||
8 | ||
9 | class AsyncSession(object): | |
10 | ||
11 | """Asynchronous msgpack-rpc layer that wraps a msgpack stream. | |
12 | ||
13 | This wraps the msgpack stream interface for reading/writing msgpack | |
14 | documents and exposes an interface for sending and receiving msgpack-rpc | |
15 | requests and notifications. | |
16 | """ | |
17 | ||
18 | def __init__(self, msgpack_stream): | |
19 | """Wrap `msgpack_stream` on a msgpack-rpc interface.""" | |
20 | self._msgpack_stream = msgpack_stream | |
21 | self._next_request_id = 1 | |
22 | self._pending_requests = {} | |
23 | self._request_cb = self._notification_cb = None | |
24 | self._handlers = { | |
25 | 0: self._on_request, | |
26 | 1: self._on_response, | |
27 | 2: self._on_notification | |
28 | } | |
29 | self.loop = msgpack_stream.loop | |
30 | ||
31 | def threadsafe_call(self, fn): | |
32 | """Wrapper around `MsgpackStream.threadsafe_call`.""" | |
33 | self._msgpack_stream.threadsafe_call(fn) | |
34 | ||
35 | def request(self, method, args, response_cb): | |
36 | """Send a msgpack-rpc request to Nvim. | |
37 | ||
38 | A msgpack-rpc with method `method` and argument `args` is sent to | |
39 | Nvim. The `response_cb` function is called with when the response | |
40 | is available. | |
41 | """ | |
42 | request_id = self._next_request_id | |
43 | self._next_request_id = request_id + 1 | |
44 | self._msgpack_stream.send([0, request_id, method, args]) | |
45 | self._pending_requests[request_id] = response_cb | |
46 | ||
47 | def notify(self, method, args): | |
48 | """Send a msgpack-rpc notification to Nvim. | |
49 | ||
50 | A msgpack-rpc with method `method` and argument `args` is sent to | |
51 | Nvim. This will have the same effect as a request, but no response | |
52 | will be recieved | |
53 | """ | |
54 | self._msgpack_stream.send([2, method, args]) | |
55 | ||
56 | def run(self, request_cb, notification_cb): | |
57 | """Run the event loop to receive requests and notifications from Nvim. | |
58 | ||
59 | While the event loop is running, `request_cb` and `_notification_cb` | |
60 | will be called whenever requests or notifications are respectively | |
61 | available. | |
62 | """ | |
63 | self._request_cb = request_cb | |
64 | self._notification_cb = notification_cb | |
65 | self._msgpack_stream.run(self._on_message) | |
66 | self._request_cb = None | |
67 | self._notification_cb = None | |
68 | ||
69 | def stop(self): | |
70 | """Stop the event loop.""" | |
71 | self._msgpack_stream.stop() | |
72 | ||
73 | def close(self): | |
74 | """Close the event loop.""" | |
75 | self._msgpack_stream.close() | |
76 | ||
77 | def _on_message(self, msg): | |
78 | try: | |
79 | self._handlers.get(msg[0], self._on_invalid_message)(msg) | |
80 | except Exception: | |
81 | err_str = format_exc(5) | |
82 | warn(err_str) | |
83 | self._msgpack_stream.send([1, 0, err_str, None]) | |
84 | ||
85 | def _on_request(self, msg): | |
86 | # request | |
87 | # - msg[1]: id | |
88 | # - msg[2]: method name | |
89 | # - msg[3]: arguments | |
90 | debug('received request: %s, %s', msg[2], msg[3]) | |
91 | self._request_cb(msg[2], msg[3], Response(self._msgpack_stream, | |
92 | msg[1])) | |
93 | ||
94 | def _on_response(self, msg): | |
95 | # response to a previous request: | |
96 | # - msg[1]: the id | |
97 | # - msg[2]: error(if any) | |
98 | # - msg[3]: result(if not errored) | |
99 | debug('received response: %s, %s', msg[2], msg[3]) | |
100 | self._pending_requests.pop(msg[1])(msg[2], msg[3]) | |
101 | ||
102 | def _on_notification(self, msg): | |
103 | # notification/event | |
104 | # - msg[1]: event name | |
105 | # - msg[2]: arguments | |
106 | debug('received notification: %s, %s', msg[1], msg[2]) | |
107 | self._notification_cb(msg[1], msg[2]) | |
108 | ||
109 | def _on_invalid_message(self, msg): | |
110 | error = 'Received invalid message %s' % msg | |
111 | warn(error) | |
112 | self._msgpack_stream.send([1, 0, error, None]) | |
113 | ||
114 | ||
115 | class Response(object): | |
116 | ||
117 | """Response to a msgpack-rpc request that came from Nvim. | |
118 | ||
119 | When Nvim sends a msgpack-rpc request, an instance of this class is | |
120 | created for remembering state required to send a response. | |
121 | """ | |
122 | ||
123 | def __init__(self, msgpack_stream, request_id): | |
124 | """Initialize the Response instance.""" | |
125 | self._msgpack_stream = msgpack_stream | |
126 | self._request_id = request_id | |
127 | ||
128 | def send(self, value, error=False): | |
129 | """Send the response. | |
130 | ||
131 | If `error` is True, it will be sent as an error. | |
132 | """ | |
133 | if error: | |
134 | resp = [1, self._request_id, value, None] | |
135 | else: | |
136 | resp = [1, self._request_id, None, value] | |
137 | debug('sending response to request %d: %s', self._request_id, resp) | |
138 | self._msgpack_stream.send(resp) |
0 | """Event loop abstraction subpackage. | |
1 | ||
2 | Tries to use pyuv as a backend, falling back to the asyncio implementation. | |
3 | """ | |
4 | ||
5 | from ...compat import IS_PYTHON3 | |
6 | ||
7 | # on python3 we only support asyncio, as we expose it to plugins | |
8 | if IS_PYTHON3: | |
9 | from .asyncio import AsyncioEventLoop | |
10 | EventLoop = AsyncioEventLoop | |
11 | else: | |
12 | try: | |
13 | # libuv is fully implemented in C, use it when available | |
14 | from .uv import UvEventLoop | |
15 | EventLoop = UvEventLoop | |
16 | except ImportError: | |
17 | # asyncio(trollius on python 2) is pure python and should be more | |
18 | # portable across python implementations | |
19 | from .asyncio import AsyncioEventLoop | |
20 | EventLoop = AsyncioEventLoop | |
21 | ||
22 | ||
23 | __all__ = ('EventLoop') |
0 | """Event loop implementation that uses the `asyncio` standard module. | |
1 | ||
2 | The `asyncio` module was added to python standard library on 3.4, and it | |
3 | provides a pure python implementation of an event loop library. It is used | |
4 | as a fallback in case pyuv is not available(on python implementations other | |
5 | than CPython). | |
6 | ||
7 | Earlier python versions are supported through the `trollius` package, which | |
8 | is a backport of `asyncio` that works on Python 2.6+. | |
9 | """ | |
10 | from __future__ import absolute_import | |
11 | ||
12 | import logging | |
13 | import os | |
14 | import sys | |
15 | from collections import deque | |
16 | ||
17 | try: | |
18 | # For python 3.4+, use the standard library module | |
19 | import asyncio | |
20 | except (ImportError, SyntaxError): | |
21 | # Fallback to trollius | |
22 | import trollius as asyncio | |
23 | ||
24 | from .base import BaseEventLoop | |
25 | ||
26 | logger = logging.getLogger(__name__) | |
27 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
28 | ||
29 | loop_cls = asyncio.SelectorEventLoop | |
30 | if os.name == 'nt': | |
31 | from asyncio.windows_utils import PipeHandle | |
32 | import msvcrt | |
33 | ||
34 | # On windows use ProactorEventLoop which support pipes and is backed by the | |
35 | # more powerful IOCP facility | |
36 | # NOTE: we override in the stdio case, because it doesn't work. | |
37 | loop_cls = asyncio.ProactorEventLoop | |
38 | ||
39 | ||
40 | class AsyncioEventLoop(BaseEventLoop, asyncio.Protocol, | |
41 | asyncio.SubprocessProtocol): | |
42 | ||
43 | """`BaseEventLoop` subclass that uses `asyncio` as a backend.""" | |
44 | ||
45 | def connection_made(self, transport): | |
46 | """Used to signal `asyncio.Protocol` of a successful connection.""" | |
47 | self._transport = transport | |
48 | self._raw_transport = transport | |
49 | if isinstance(transport, asyncio.SubprocessTransport): | |
50 | self._transport = transport.get_pipe_transport(0) | |
51 | ||
52 | def connection_lost(self, exc): | |
53 | """Used to signal `asyncio.Protocol` of a lost connection.""" | |
54 | self._on_error(exc.args[0] if exc else 'EOF') | |
55 | ||
56 | def data_received(self, data): | |
57 | """Used to signal `asyncio.Protocol` of incoming data.""" | |
58 | if self._on_data: | |
59 | self._on_data(data) | |
60 | return | |
61 | self._queued_data.append(data) | |
62 | ||
63 | def pipe_connection_lost(self, fd, exc): | |
64 | """Used to signal `asyncio.SubprocessProtocol` of a lost connection.""" | |
65 | self._on_error(exc.args[0] if exc else 'EOF') | |
66 | ||
67 | def pipe_data_received(self, fd, data): | |
68 | """Used to signal `asyncio.SubprocessProtocol` of incoming data.""" | |
69 | if fd == 2: # stderr fd number | |
70 | self._on_stderr(data) | |
71 | elif self._on_data: | |
72 | self._on_data(data) | |
73 | else: | |
74 | self._queued_data.append(data) | |
75 | ||
76 | def process_exited(self): | |
77 | """Used to signal `asyncio.SubprocessProtocol` when the child exits.""" | |
78 | self._on_error('EOF') | |
79 | ||
80 | def _init(self): | |
81 | self._loop = loop_cls() | |
82 | self._queued_data = deque() | |
83 | self._fact = lambda: self | |
84 | self._raw_transport = None | |
85 | ||
86 | def _connect_tcp(self, address, port): | |
87 | coroutine = self._loop.create_connection(self._fact, address, port) | |
88 | self._loop.run_until_complete(coroutine) | |
89 | ||
90 | def _connect_socket(self, path): | |
91 | if os.name == 'nt': | |
92 | coroutine = self._loop.create_pipe_connection(self._fact, path) | |
93 | else: | |
94 | coroutine = self._loop.create_unix_connection(self._fact, path) | |
95 | self._loop.run_until_complete(coroutine) | |
96 | ||
97 | def _connect_stdio(self): | |
98 | if os.name == 'nt': | |
99 | pipe = PipeHandle(msvcrt.get_osfhandle(sys.stdin.fileno())) | |
100 | else: | |
101 | pipe = sys.stdin | |
102 | coroutine = self._loop.connect_read_pipe(self._fact, pipe) | |
103 | self._loop.run_until_complete(coroutine) | |
104 | debug("native stdin connection successful") | |
105 | ||
106 | # Make sure subprocesses don't clobber stdout, | |
107 | # send the output to stderr instead. | |
108 | rename_stdout = os.dup(sys.stdout.fileno()) | |
109 | os.dup2(sys.stderr.fileno(), sys.stdout.fileno()) | |
110 | ||
111 | if os.name == 'nt': | |
112 | pipe = PipeHandle(msvcrt.get_osfhandle(rename_stdout)) | |
113 | else: | |
114 | pipe = os.fdopen(rename_stdout, 'wb') | |
115 | coroutine = self._loop.connect_write_pipe(self._fact, pipe) | |
116 | self._loop.run_until_complete(coroutine) | |
117 | debug("native stdout connection successful") | |
118 | ||
119 | def _connect_child(self, argv): | |
120 | if os.name != 'nt': | |
121 | self._child_watcher = asyncio.get_child_watcher() | |
122 | self._child_watcher.attach_loop(self._loop) | |
123 | coroutine = self._loop.subprocess_exec(self._fact, *argv) | |
124 | self._loop.run_until_complete(coroutine) | |
125 | ||
126 | def _start_reading(self): | |
127 | pass | |
128 | ||
129 | def _send(self, data): | |
130 | self._transport.write(data) | |
131 | ||
132 | def _run(self): | |
133 | while self._queued_data: | |
134 | self._on_data(self._queued_data.popleft()) | |
135 | self._loop.run_forever() | |
136 | ||
137 | def _stop(self): | |
138 | self._loop.stop() | |
139 | ||
140 | def _close(self): | |
141 | if self._raw_transport is not None: | |
142 | self._raw_transport.close() | |
143 | self._loop.close() | |
144 | ||
145 | def _threadsafe_call(self, fn): | |
146 | self._loop.call_soon_threadsafe(fn) | |
147 | ||
148 | def _setup_signals(self, signals): | |
149 | if os.name == 'nt': | |
150 | # add_signal_handler is not supported in win32 | |
151 | self._signals = [] | |
152 | return | |
153 | ||
154 | self._signals = list(signals) | |
155 | for signum in self._signals: | |
156 | self._loop.add_signal_handler(signum, self._on_signal, signum) | |
157 | ||
158 | def _teardown_signals(self): | |
159 | for signum in self._signals: | |
160 | self._loop.remove_signal_handler(signum) |
0 | """Common code for event loop implementations.""" | |
1 | import logging | |
2 | import signal | |
3 | import threading | |
4 | ||
5 | ||
6 | logger = logging.getLogger(__name__) | |
7 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
8 | ||
9 | ||
10 | # When signals are restored, the event loop library may reset SIGINT to SIG_DFL | |
11 | # which exits the program. To be able to restore the python interpreter to it's | |
12 | # default state, we keep a reference to the default handler | |
13 | default_int_handler = signal.getsignal(signal.SIGINT) | |
14 | main_thread = threading.current_thread() | |
15 | ||
16 | ||
17 | class BaseEventLoop(object): | |
18 | ||
19 | """Abstract base class for all event loops. | |
20 | ||
21 | Event loops act as the bottom layer for Nvim sessions created by this | |
22 | library. They hide system/transport details behind a simple interface for | |
23 | reading/writing bytes to the connected Nvim instance. | |
24 | ||
25 | This class exposes public methods for interacting with the underlying | |
26 | event loop and delegates implementation-specific work to the following | |
27 | methods, which subclasses are expected to implement: | |
28 | ||
29 | - `_init()`: Implementation-specific initialization | |
30 | - `_connect_tcp(address, port)`: connect to Nvim using tcp/ip | |
31 | - `_connect_socket(path)`: Same as tcp, but use a UNIX domain socket or | |
32 | named pipe. | |
33 | - `_connect_stdio()`: Use stdin/stdout as the connection to Nvim | |
34 | - `_connect_child(argv)`: Use the argument vector `argv` to spawn an | |
35 | embedded Nvim that has it's stdin/stdout connected to the event loop. | |
36 | - `_start_reading()`: Called after any of _connect_* methods. Can be used | |
37 | to perform any post-connection setup or validation. | |
38 | - `_send(data)`: Send `data`(byte array) to Nvim. The data is only | |
39 | - `_run()`: Runs the event loop until stopped or the connection is closed. | |
40 | calling the following methods when some event happens: | |
41 | actually sent when the event loop is running. | |
42 | - `_on_data(data)`: When Nvim sends some data. | |
43 | - `_on_signal(signum)`: When a signal is received. | |
44 | - `_on_error(message)`: When a non-recoverable error occurs(eg: | |
45 | connection lost) | |
46 | - `_stop()`: Stop the event loop | |
47 | - `_interrupt(data)`: Like `stop()`, but may be called from other threads | |
48 | this. | |
49 | - `_setup_signals(signals)`: Add implementation-specific listeners for | |
50 | for `signals`, which is a list of OS-specific signal numbers. | |
51 | - `_teardown_signals()`: Removes signal listeners set by `_setup_signals` | |
52 | """ | |
53 | ||
54 | def __init__(self, transport_type, *args): | |
55 | """Initialize and connect the event loop instance. | |
56 | ||
57 | The only arguments are the transport type and transport-specific | |
58 | configuration, like this: | |
59 | ||
60 | >>> BaseEventLoop('tcp', '127.0.0.1', 7450) | |
61 | Traceback (most recent call last): | |
62 | ... | |
63 | AttributeError: 'BaseEventLoop' object has no attribute '_init' | |
64 | >>> BaseEventLoop('socket', '/tmp/nvim-socket') | |
65 | Traceback (most recent call last): | |
66 | ... | |
67 | AttributeError: 'BaseEventLoop' object has no attribute '_init' | |
68 | >>> BaseEventLoop('stdio') | |
69 | Traceback (most recent call last): | |
70 | ... | |
71 | AttributeError: 'BaseEventLoop' object has no attribute '_init' | |
72 | >>> BaseEventLoop('child', ['nvim', '--embed', '-u', 'NONE']) | |
73 | Traceback (most recent call last): | |
74 | ... | |
75 | AttributeError: 'BaseEventLoop' object has no attribute '_init' | |
76 | ||
77 | This calls the implementation-specific initialization | |
78 | `_init`, one of the `_connect_*` methods(based on `transport_type`) | |
79 | and `_start_reading()` | |
80 | """ | |
81 | self._transport_type = transport_type | |
82 | self._signames = dict((k, v) for v, k in signal.__dict__.items() | |
83 | if v.startswith('SIG')) | |
84 | self._on_data = None | |
85 | self._error = None | |
86 | self._init() | |
87 | try: | |
88 | getattr(self, '_connect_{}'.format(transport_type))(*args) | |
89 | except Exception as e: | |
90 | self.close() | |
91 | raise e | |
92 | self._start_reading() | |
93 | ||
94 | def connect_tcp(self, address, port): | |
95 | """Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`.""" | |
96 | info('Connecting to TCP address: %s:%d', address, port) | |
97 | self._connect_tcp(address, port) | |
98 | ||
99 | def connect_socket(self, path): | |
100 | """Connect to socket at `path`. Delegated to `_connect_socket`.""" | |
101 | info('Connecting to %s', path) | |
102 | self._connect_socket(path) | |
103 | ||
104 | def connect_stdio(self): | |
105 | """Connect using stdin/stdout. Delegated to `_connect_stdio`.""" | |
106 | info('Preparing stdin/stdout for streaming data') | |
107 | self._connect_stdio() | |
108 | ||
109 | def connect_child(self, argv): | |
110 | """Connect a new Nvim instance. Delegated to `_connect_child`.""" | |
111 | info('Spawning a new nvim instance') | |
112 | self._connect_child(argv) | |
113 | ||
114 | def send(self, data): | |
115 | """Queue `data` for sending to Nvim.""" | |
116 | debug("Sending '%s'", data) | |
117 | self._send(data) | |
118 | ||
119 | def threadsafe_call(self, fn): | |
120 | """Call a function in the event loop thread. | |
121 | ||
122 | This is the only safe way to interact with a session from other | |
123 | threads. | |
124 | """ | |
125 | self._threadsafe_call(fn) | |
126 | ||
127 | def run(self, data_cb): | |
128 | """Run the event loop.""" | |
129 | if self._error: | |
130 | err = self._error | |
131 | if isinstance(self._error, KeyboardInterrupt): | |
132 | # KeyboardInterrupt is not destructive(it may be used in | |
133 | # the REPL). | |
134 | # After throwing KeyboardInterrupt, cleanup the _error field | |
135 | # so the loop may be started again | |
136 | self._error = None | |
137 | raise err | |
138 | self._on_data = data_cb | |
139 | if threading.current_thread() == main_thread: | |
140 | self._setup_signals([signal.SIGINT, signal.SIGTERM]) | |
141 | debug('Entering event loop') | |
142 | self._run() | |
143 | debug('Exited event loop') | |
144 | if threading.current_thread() == main_thread: | |
145 | self._teardown_signals() | |
146 | signal.signal(signal.SIGINT, default_int_handler) | |
147 | self._on_data = None | |
148 | ||
149 | def stop(self): | |
150 | """Stop the event loop.""" | |
151 | self._stop() | |
152 | debug('Stopped event loop') | |
153 | ||
154 | def close(self): | |
155 | """Stop the event loop.""" | |
156 | self._close() | |
157 | debug('Closed event loop') | |
158 | ||
159 | def _on_signal(self, signum): | |
160 | msg = 'Received {}'.format(self._signames[signum]) | |
161 | debug(msg) | |
162 | if signum == signal.SIGINT and self._transport_type == 'stdio': | |
163 | # When the transport is stdio, we are probably running as a Nvim | |
164 | # child process. In that case, we don't want to be killed by | |
165 | # ctrl+C | |
166 | return | |
167 | cls = Exception | |
168 | if signum == signal.SIGINT: | |
169 | cls = KeyboardInterrupt | |
170 | self._error = cls(msg) | |
171 | self.stop() | |
172 | ||
173 | def _on_error(self, error): | |
174 | debug(error) | |
175 | self._error = IOError(error) | |
176 | self.stop() | |
177 | ||
178 | def _on_interrupt(self): | |
179 | self.stop() |
0 | """Event loop implementation that uses pyuv(libuv-python bindings).""" | |
1 | import sys | |
2 | from collections import deque | |
3 | ||
4 | import pyuv | |
5 | ||
6 | from .base import BaseEventLoop | |
7 | ||
8 | ||
9 | class UvEventLoop(BaseEventLoop): | |
10 | ||
11 | """`BaseEventLoop` subclass that uses `pvuv` as a backend.""" | |
12 | ||
13 | def _init(self): | |
14 | self._loop = pyuv.Loop() | |
15 | self._async = pyuv.Async(self._loop, self._on_async) | |
16 | self._connection_error = None | |
17 | self._error_stream = None | |
18 | self._callbacks = deque() | |
19 | ||
20 | def _on_connect(self, stream, error): | |
21 | self.stop() | |
22 | if error: | |
23 | msg = 'Cannot connect to {}: {}'.format( | |
24 | self._connect_address, pyuv.errno.strerror(error)) | |
25 | self._connection_error = IOError(msg) | |
26 | return | |
27 | self._read_stream = self._write_stream = stream | |
28 | ||
29 | def _on_read(self, handle, data, error): | |
30 | if error or not data: | |
31 | msg = pyuv.errno.strerror(error) if error else 'EOF' | |
32 | self._on_error(msg) | |
33 | return | |
34 | if handle == self._error_stream: | |
35 | return | |
36 | self._on_data(data) | |
37 | ||
38 | def _on_write(self, handle, error): | |
39 | if error: | |
40 | msg = pyuv.errno.strerror(error) | |
41 | self._on_error(msg) | |
42 | ||
43 | def _on_exit(self, handle, exit_status, term_signal): | |
44 | self._on_error('EOF') | |
45 | ||
46 | def _disconnected(self, *args): | |
47 | raise IOError('Not connected to Nvim') | |
48 | ||
49 | def _connect_tcp(self, address, port): | |
50 | stream = pyuv.TCP(self._loop) | |
51 | self._connect_address = '{}:{}'.format(address, port) | |
52 | stream.connect((address, port), self._on_connect) | |
53 | ||
54 | def _connect_socket(self, path): | |
55 | stream = pyuv.Pipe(self._loop) | |
56 | self._connect_address = path | |
57 | stream.connect(path, self._on_connect) | |
58 | ||
59 | def _connect_stdio(self): | |
60 | self._read_stream = pyuv.Pipe(self._loop) | |
61 | self._read_stream.open(sys.stdin.fileno()) | |
62 | self._write_stream = pyuv.Pipe(self._loop) | |
63 | self._write_stream.open(sys.stdout.fileno()) | |
64 | ||
65 | def _connect_child(self, argv): | |
66 | self._write_stream = pyuv.Pipe(self._loop) | |
67 | self._read_stream = pyuv.Pipe(self._loop) | |
68 | self._error_stream = pyuv.Pipe(self._loop) | |
69 | stdin = pyuv.StdIO(self._write_stream, | |
70 | flags=pyuv.UV_CREATE_PIPE + pyuv.UV_READABLE_PIPE) | |
71 | stdout = pyuv.StdIO(self._read_stream, | |
72 | flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE) | |
73 | stderr = pyuv.StdIO(self._error_stream, | |
74 | flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE) | |
75 | pyuv.Process.spawn(self._loop, | |
76 | args=argv, | |
77 | exit_callback=self._on_exit, | |
78 | flags=pyuv.UV_PROCESS_WINDOWS_HIDE, | |
79 | stdio=(stdin, stdout, stderr,)) | |
80 | self._error_stream.start_read(self._on_read) | |
81 | ||
82 | def _start_reading(self): | |
83 | if self._transport_type in ['tcp', 'socket']: | |
84 | self._loop.run() | |
85 | if self._connection_error: | |
86 | self.run = self.send = self._disconnected | |
87 | raise self._connection_error | |
88 | self._read_stream.start_read(self._on_read) | |
89 | ||
90 | def _send(self, data): | |
91 | self._write_stream.write(data, self._on_write) | |
92 | ||
93 | def _run(self): | |
94 | self._loop.run(pyuv.UV_RUN_DEFAULT) | |
95 | ||
96 | def _stop(self): | |
97 | self._loop.stop() | |
98 | ||
99 | def _close(self): | |
100 | pass | |
101 | ||
102 | def _threadsafe_call(self, fn): | |
103 | self._callbacks.append(fn) | |
104 | self._async.send() | |
105 | ||
106 | def _on_async(self, handle): | |
107 | while self._callbacks: | |
108 | self._callbacks.popleft()() | |
109 | ||
110 | def _setup_signals(self, signals): | |
111 | self._signal_handles = [] | |
112 | ||
113 | def handler(h, signum): | |
114 | self._on_signal(signum) | |
115 | ||
116 | for signum in signals: | |
117 | handle = pyuv.Signal(self._loop) | |
118 | handle.start(handler, signum) | |
119 | self._signal_handles.append(handle) | |
120 | ||
121 | def _teardown_signals(self): | |
122 | for handle in self._signal_handles: | |
123 | handle.stop() |
0 | """Msgpack handling in the event loop pipeline.""" | |
1 | import logging | |
2 | ||
3 | from msgpack import Packer, Unpacker | |
4 | ||
5 | from ..compat import unicode_errors_default | |
6 | ||
7 | logger = logging.getLogger(__name__) | |
8 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
9 | ||
10 | ||
11 | class MsgpackStream(object): | |
12 | ||
13 | """Two-way msgpack stream that wraps a event loop byte stream. | |
14 | ||
15 | This wraps the event loop interface for reading/writing bytes and | |
16 | exposes an interface for reading/writing msgpack documents. | |
17 | """ | |
18 | ||
19 | def __init__(self, event_loop): | |
20 | """Wrap `event_loop` on a msgpack-aware interface.""" | |
21 | self.loop = event_loop | |
22 | self._packer = Packer(unicode_errors=unicode_errors_default) | |
23 | self._unpacker = Unpacker() | |
24 | self._message_cb = None | |
25 | ||
26 | def threadsafe_call(self, fn): | |
27 | """Wrapper around `BaseEventLoop.threadsafe_call`.""" | |
28 | self.loop.threadsafe_call(fn) | |
29 | ||
30 | def send(self, msg): | |
31 | """Queue `msg` for sending to Nvim.""" | |
32 | debug('sent %s', msg) | |
33 | self.loop.send(self._packer.pack(msg)) | |
34 | ||
35 | def run(self, message_cb): | |
36 | """Run the event loop to receive messages from Nvim. | |
37 | ||
38 | While the event loop is running, `message_cb` will be called whenever | |
39 | a message has been successfully parsed from the input stream. | |
40 | """ | |
41 | self._message_cb = message_cb | |
42 | self.loop.run(self._on_data) | |
43 | self._message_cb = None | |
44 | ||
45 | def stop(self): | |
46 | """Stop the event loop.""" | |
47 | self.loop.stop() | |
48 | ||
49 | def close(self): | |
50 | """Close the event loop.""" | |
51 | self.loop.close() | |
52 | ||
53 | def _on_data(self, data): | |
54 | self._unpacker.feed(data) | |
55 | while True: | |
56 | try: | |
57 | debug('waiting for message...') | |
58 | msg = next(self._unpacker) | |
59 | debug('received message: %s', msg) | |
60 | self._message_cb(msg) | |
61 | except StopIteration: | |
62 | debug('unpacker needs more data...') | |
63 | break |
0 | """Synchronous msgpack-rpc session layer.""" | |
1 | import logging | |
2 | import threading | |
3 | from collections import deque | |
4 | from traceback import format_exc | |
5 | ||
6 | import greenlet | |
7 | ||
8 | from ..compat import check_async | |
9 | ||
10 | logger = logging.getLogger(__name__) | |
11 | error, debug, info, warn = (logger.error, logger.debug, logger.info, | |
12 | logger.warning,) | |
13 | ||
14 | ||
15 | class Session(object): | |
16 | ||
17 | """Msgpack-rpc session layer that uses coroutines for a synchronous API. | |
18 | ||
19 | This class provides the public msgpack-rpc API required by this library. | |
20 | It uses the greenlet module to handle requests and notifications coming | |
21 | from Nvim with a synchronous API. | |
22 | """ | |
23 | ||
24 | def __init__(self, async_session): | |
25 | """Wrap `async_session` on a synchronous msgpack-rpc interface.""" | |
26 | self._async_session = async_session | |
27 | self._request_cb = self._notification_cb = None | |
28 | self._pending_messages = deque() | |
29 | self._is_running = False | |
30 | self._setup_exception = None | |
31 | self.loop = async_session.loop | |
32 | self._loop_thread = None | |
33 | ||
34 | def threadsafe_call(self, fn, *args, **kwargs): | |
35 | """Wrapper around `AsyncSession.threadsafe_call`.""" | |
36 | def handler(): | |
37 | try: | |
38 | fn(*args, **kwargs) | |
39 | except Exception: | |
40 | warn("error caught while excecuting async callback\n%s\n", | |
41 | format_exc()) | |
42 | ||
43 | def greenlet_wrapper(): | |
44 | gr = greenlet.greenlet(handler) | |
45 | gr.switch() | |
46 | ||
47 | self._async_session.threadsafe_call(greenlet_wrapper) | |
48 | ||
49 | def next_message(self): | |
50 | """Block until a message(request or notification) is available. | |
51 | ||
52 | If any messages were previously enqueued, return the first in queue. | |
53 | If not, run the event loop until one is received. | |
54 | """ | |
55 | if self._is_running: | |
56 | raise Exception('Event loop already running') | |
57 | if self._pending_messages: | |
58 | return self._pending_messages.popleft() | |
59 | self._async_session.run(self._enqueue_request_and_stop, | |
60 | self._enqueue_notification_and_stop) | |
61 | if self._pending_messages: | |
62 | return self._pending_messages.popleft() | |
63 | ||
64 | def request(self, method, *args, **kwargs): | |
65 | """Send a msgpack-rpc request and block until as response is received. | |
66 | ||
67 | If the event loop is running, this method must have been called by a | |
68 | request or notification handler running on a greenlet. In that case, | |
69 | send the quest and yield to the parent greenlet until a response is | |
70 | available. | |
71 | ||
72 | When the event loop is not running, it will perform a blocking request | |
73 | like this: | |
74 | - Send the request | |
75 | - Run the loop until the response is available | |
76 | - Put requests/notifications received while waiting into a queue | |
77 | ||
78 | If the `async_` flag is present and True, a asynchronous notification | |
79 | is sent instead. This will never block, and the return value or error | |
80 | is ignored. | |
81 | """ | |
82 | async_ = check_async(kwargs.pop('async_', None), kwargs, False) | |
83 | if async_: | |
84 | self._async_session.notify(method, args) | |
85 | return | |
86 | ||
87 | if kwargs: | |
88 | raise ValueError("request got unsupported keyword argument(s): {}" | |
89 | .format(', '.join(kwargs.keys()))) | |
90 | ||
91 | if self._is_running: | |
92 | v = self._yielding_request(method, args) | |
93 | else: | |
94 | v = self._blocking_request(method, args) | |
95 | if not v: | |
96 | # EOF | |
97 | raise IOError('EOF') | |
98 | err, rv = v | |
99 | if err: | |
100 | info("'Received error: %s", err) | |
101 | raise self.error_wrapper(err) | |
102 | return rv | |
103 | ||
104 | def run(self, request_cb, notification_cb, setup_cb=None): | |
105 | """Run the event loop to receive requests and notifications from Nvim. | |
106 | ||
107 | Like `AsyncSession.run()`, but `request_cb` and `notification_cb` are | |
108 | inside greenlets. | |
109 | """ | |
110 | self._request_cb = request_cb | |
111 | self._notification_cb = notification_cb | |
112 | self._is_running = True | |
113 | self._setup_exception = None | |
114 | self._loop_thread = threading.current_thread() | |
115 | ||
116 | def on_setup(): | |
117 | try: | |
118 | setup_cb() | |
119 | except Exception as e: | |
120 | self._setup_exception = e | |
121 | self.stop() | |
122 | ||
123 | if setup_cb: | |
124 | # Create a new greenlet to handle the setup function | |
125 | gr = greenlet.greenlet(on_setup) | |
126 | gr.switch() | |
127 | ||
128 | if self._setup_exception: | |
129 | error('Setup error: {}'.format(self._setup_exception)) | |
130 | raise self._setup_exception | |
131 | ||
132 | # Process all pending requests and notifications | |
133 | while self._pending_messages: | |
134 | msg = self._pending_messages.popleft() | |
135 | getattr(self, '_on_{}'.format(msg[0]))(*msg[1:]) | |
136 | self._async_session.run(self._on_request, self._on_notification) | |
137 | self._is_running = False | |
138 | self._request_cb = None | |
139 | self._notification_cb = None | |
140 | self._loop_thread = None | |
141 | ||
142 | if self._setup_exception: | |
143 | raise self._setup_exception | |
144 | ||
145 | def stop(self): | |
146 | """Stop the event loop.""" | |
147 | self._async_session.stop() | |
148 | ||
149 | def close(self): | |
150 | """Close the event loop.""" | |
151 | self._async_session.close() | |
152 | ||
153 | def _yielding_request(self, method, args): | |
154 | gr = greenlet.getcurrent() | |
155 | parent = gr.parent | |
156 | ||
157 | def response_cb(err, rv): | |
158 | debug('response is available for greenlet %s, switching back', gr) | |
159 | gr.switch(err, rv) | |
160 | ||
161 | self._async_session.request(method, args, response_cb) | |
162 | debug('yielding from greenlet %s to wait for response', gr) | |
163 | return parent.switch() | |
164 | ||
165 | def _blocking_request(self, method, args): | |
166 | result = [] | |
167 | ||
168 | def response_cb(err, rv): | |
169 | result.extend([err, rv]) | |
170 | self.stop() | |
171 | ||
172 | self._async_session.request(method, args, response_cb) | |
173 | self._async_session.run(self._enqueue_request, | |
174 | self._enqueue_notification) | |
175 | return result | |
176 | ||
177 | def _enqueue_request_and_stop(self, name, args, response): | |
178 | self._enqueue_request(name, args, response) | |
179 | self.stop() | |
180 | ||
181 | def _enqueue_notification_and_stop(self, name, args): | |
182 | self._enqueue_notification(name, args) | |
183 | self.stop() | |
184 | ||
185 | def _enqueue_request(self, name, args, response): | |
186 | self._pending_messages.append(('request', name, args, response,)) | |
187 | ||
188 | def _enqueue_notification(self, name, args): | |
189 | self._pending_messages.append(('notification', name, args,)) | |
190 | ||
191 | def _on_request(self, name, args, response): | |
192 | def handler(): | |
193 | try: | |
194 | rv = self._request_cb(name, args) | |
195 | debug('greenlet %s finished executing, ' | |
196 | + 'sending %s as response', gr, rv) | |
197 | response.send(rv) | |
198 | except ErrorResponse as err: | |
199 | warn("error response from request '%s %s': %s", name, | |
200 | args, format_exc()) | |
201 | response.send(err.args[0], error=True) | |
202 | except Exception as err: | |
203 | warn("error caught while processing request '%s %s': %s", name, | |
204 | args, format_exc()) | |
205 | response.send(repr(err) + "\n" + format_exc(5), error=True) | |
206 | debug('greenlet %s is now dying...', gr) | |
207 | ||
208 | # Create a new greenlet to handle the request | |
209 | gr = greenlet.greenlet(handler) | |
210 | debug('received rpc request, greenlet %s will handle it', gr) | |
211 | gr.switch() | |
212 | ||
213 | def _on_notification(self, name, args): | |
214 | def handler(): | |
215 | try: | |
216 | self._notification_cb(name, args) | |
217 | debug('greenlet %s finished executing', gr) | |
218 | except Exception: | |
219 | warn("error caught while processing notification '%s %s': %s", | |
220 | name, args, format_exc()) | |
221 | ||
222 | debug('greenlet %s is now dying...', gr) | |
223 | ||
224 | gr = greenlet.greenlet(handler) | |
225 | debug('received rpc notification, greenlet %s will handle it', gr) | |
226 | gr.switch() | |
227 | ||
228 | ||
229 | class ErrorResponse(BaseException): | |
230 | ||
231 | """Raise this in a request handler to respond with a given error message. | |
232 | ||
233 | Unlike when other exceptions are caught, this gives full control off the | |
234 | error response sent. When "ErrorResponse(msg)" is caught "msg" will be | |
235 | sent verbatim as the error response.No traceback will be appended. | |
236 | """ | |
237 | ||
238 | pass |
0 | """Nvim plugin/host subpackage.""" | |
1 | ||
2 | from .decorators import (autocmd, command, decode, encoding, function, | |
3 | plugin, rpc_export, shutdown_hook) | |
4 | from .host import Host | |
5 | ||
6 | ||
7 | __all__ = ('Host', 'plugin', 'rpc_export', 'command', 'autocmd', | |
8 | 'function', 'encoding', 'decode', 'shutdown_hook') |
0 | """Decorators used by python host plugin system.""" | |
1 | ||
2 | import inspect | |
3 | import logging | |
4 | ||
5 | from ..compat import IS_PYTHON3, unicode_errors_default | |
6 | ||
7 | logger = logging.getLogger(__name__) | |
8 | debug, info, warn = (logger.debug, logger.info, logger.warning,) | |
9 | __all__ = ('plugin', 'rpc_export', 'command', 'autocmd', 'function', | |
10 | 'encoding', 'decode', 'shutdown_hook') | |
11 | ||
12 | ||
13 | def plugin(cls): | |
14 | """Tag a class as a plugin. | |
15 | ||
16 | This decorator is required to make the class methods discoverable by the | |
17 | plugin_load method of the host. | |
18 | """ | |
19 | cls._nvim_plugin = True | |
20 | # the _nvim_bind attribute is set to True by default, meaning that | |
21 | # decorated functions have a bound Nvim instance as first argument. | |
22 | # For methods in a plugin-decorated class this is not required, because | |
23 | # the class initializer will already receive the nvim object. | |
24 | predicate = lambda fn: hasattr(fn, '_nvim_bind') | |
25 | for _, fn in inspect.getmembers(cls, predicate): | |
26 | if IS_PYTHON3: | |
27 | fn._nvim_bind = False | |
28 | else: | |
29 | fn.im_func._nvim_bind = False | |
30 | return cls | |
31 | ||
32 | ||
33 | def rpc_export(rpc_method_name, sync=False): | |
34 | """Export a function or plugin method as a msgpack-rpc request handler.""" | |
35 | def dec(f): | |
36 | f._nvim_rpc_method_name = rpc_method_name | |
37 | f._nvim_rpc_sync = sync | |
38 | f._nvim_bind = True | |
39 | f._nvim_prefix_plugin_path = False | |
40 | return f | |
41 | return dec | |
42 | ||
43 | ||
44 | def command(name, nargs=0, complete=None, range=None, count=None, bang=False, | |
45 | register=False, sync=False, allow_nested=False, eval=None): | |
46 | """Tag a function or plugin method as a Nvim command handler.""" | |
47 | def dec(f): | |
48 | f._nvim_rpc_method_name = 'command:{}'.format(name) | |
49 | f._nvim_rpc_sync = sync | |
50 | f._nvim_bind = True | |
51 | f._nvim_prefix_plugin_path = True | |
52 | ||
53 | opts = {} | |
54 | ||
55 | if range is not None: | |
56 | opts['range'] = '' if range is True else str(range) | |
57 | elif count is not None: | |
58 | opts['count'] = count | |
59 | ||
60 | if bang: | |
61 | opts['bang'] = '' | |
62 | ||
63 | if register: | |
64 | opts['register'] = '' | |
65 | ||
66 | if nargs: | |
67 | opts['nargs'] = nargs | |
68 | ||
69 | if complete: | |
70 | opts['complete'] = complete | |
71 | ||
72 | if eval: | |
73 | opts['eval'] = eval | |
74 | ||
75 | if not sync and allow_nested: | |
76 | rpc_sync = "urgent" | |
77 | else: | |
78 | rpc_sync = sync | |
79 | ||
80 | f._nvim_rpc_spec = { | |
81 | 'type': 'command', | |
82 | 'name': name, | |
83 | 'sync': rpc_sync, | |
84 | 'opts': opts | |
85 | } | |
86 | return f | |
87 | return dec | |
88 | ||
89 | ||
90 | def autocmd(name, pattern='*', sync=False, allow_nested=False, eval=None): | |
91 | """Tag a function or plugin method as a Nvim autocommand handler.""" | |
92 | def dec(f): | |
93 | f._nvim_rpc_method_name = 'autocmd:{}:{}'.format(name, pattern) | |
94 | f._nvim_rpc_sync = sync | |
95 | f._nvim_bind = True | |
96 | f._nvim_prefix_plugin_path = True | |
97 | ||
98 | opts = { | |
99 | 'pattern': pattern | |
100 | } | |
101 | ||
102 | if eval: | |
103 | opts['eval'] = eval | |
104 | ||
105 | if not sync and allow_nested: | |
106 | rpc_sync = "urgent" | |
107 | else: | |
108 | rpc_sync = sync | |
109 | ||
110 | f._nvim_rpc_spec = { | |
111 | 'type': 'autocmd', | |
112 | 'name': name, | |
113 | 'sync': rpc_sync, | |
114 | 'opts': opts | |
115 | } | |
116 | return f | |
117 | return dec | |
118 | ||
119 | ||
120 | def function(name, range=False, sync=False, allow_nested=False, eval=None): | |
121 | """Tag a function or plugin method as a Nvim function handler.""" | |
122 | def dec(f): | |
123 | f._nvim_rpc_method_name = 'function:{}'.format(name) | |
124 | f._nvim_rpc_sync = sync | |
125 | f._nvim_bind = True | |
126 | f._nvim_prefix_plugin_path = True | |
127 | ||
128 | opts = {} | |
129 | ||
130 | if range: | |
131 | opts['range'] = '' if range is True else str(range) | |
132 | ||
133 | if eval: | |
134 | opts['eval'] = eval | |
135 | ||
136 | if not sync and allow_nested: | |
137 | rpc_sync = "urgent" | |
138 | else: | |
139 | rpc_sync = sync | |
140 | ||
141 | f._nvim_rpc_spec = { | |
142 | 'type': 'function', | |
143 | 'name': name, | |
144 | 'sync': rpc_sync, | |
145 | 'opts': opts | |
146 | } | |
147 | return f | |
148 | return dec | |
149 | ||
150 | ||
151 | def shutdown_hook(f): | |
152 | """Tag a function or method as a shutdown hook.""" | |
153 | f._nvim_shutdown_hook = True | |
154 | f._nvim_bind = True | |
155 | return f | |
156 | ||
157 | ||
158 | def decode(mode=unicode_errors_default): | |
159 | """Configure automatic encoding/decoding of strings.""" | |
160 | def dec(f): | |
161 | f._nvim_decode = mode | |
162 | return f | |
163 | return dec | |
164 | ||
165 | ||
166 | def encoding(encoding=True): | |
167 | """DEPRECATED: use pynvim.decode().""" | |
168 | if isinstance(encoding, str): | |
169 | encoding = True | |
170 | ||
171 | def dec(f): | |
172 | f._nvim_decode = encoding | |
173 | return f | |
174 | return dec |
0 | """Implements a Nvim host for python plugins.""" | |
1 | import imp | |
2 | import inspect | |
3 | import logging | |
4 | import os | |
5 | import os.path | |
6 | import re | |
7 | import sys | |
8 | from functools import partial | |
9 | from traceback import format_exc | |
10 | ||
11 | from . import script_host | |
12 | from ..api import decode_if_bytes, walk | |
13 | from ..compat import IS_PYTHON3, find_module | |
14 | from ..msgpack_rpc import ErrorResponse | |
15 | from ..util import VERSION, format_exc_skip | |
16 | ||
17 | __all__ = ('Host') | |
18 | ||
19 | logger = logging.getLogger(__name__) | |
20 | error, debug, info, warn = (logger.error, logger.debug, logger.info, | |
21 | logger.warning,) | |
22 | ||
23 | host_method_spec = {"poll": {}, "specs": {"nargs": 1}, "shutdown": {}} | |
24 | ||
25 | ||
26 | class Host(object): | |
27 | ||
28 | """Nvim host for python plugins. | |
29 | ||
30 | Takes care of loading/unloading plugins and routing msgpack-rpc | |
31 | requests/notifications to the appropriate handlers. | |
32 | """ | |
33 | ||
34 | def __init__(self, nvim): | |
35 | """Set handlers for plugin_load/plugin_unload.""" | |
36 | self.nvim = nvim | |
37 | self._specs = {} | |
38 | self._loaded = {} | |
39 | self._load_errors = {} | |
40 | self._notification_handlers = { | |
41 | 'nvim_error_event': self._on_error_event | |
42 | } | |
43 | self._request_handlers = { | |
44 | 'poll': lambda: 'ok', | |
45 | 'specs': self._on_specs_request, | |
46 | 'shutdown': self.shutdown | |
47 | } | |
48 | ||
49 | # Decode per default for Python3 | |
50 | self._decode_default = IS_PYTHON3 | |
51 | ||
52 | def _on_async_err(self, msg): | |
53 | # uncaught python exception | |
54 | self.nvim.err_write(msg, async_=True) | |
55 | ||
56 | def _on_error_event(self, kind, msg): | |
57 | # error from nvim due to async request | |
58 | # like nvim.command(..., async_=True) | |
59 | errmsg = "{}: Async request caused an error:\n{}\n".format( | |
60 | self.name, decode_if_bytes(msg)) | |
61 | self.nvim.err_write(errmsg, async_=True) | |
62 | ||
63 | def start(self, plugins): | |
64 | """Start listening for msgpack-rpc requests and notifications.""" | |
65 | self.nvim.run_loop(self._on_request, | |
66 | self._on_notification, | |
67 | lambda: self._load(plugins), | |
68 | err_cb=self._on_async_err) | |
69 | ||
70 | def shutdown(self): | |
71 | """Shutdown the host.""" | |
72 | self._unload() | |
73 | self.nvim.stop_loop() | |
74 | ||
75 | def _wrap_function(self, fn, sync, decode, nvim_bind, name, *args): | |
76 | if decode: | |
77 | args = walk(decode_if_bytes, args, decode) | |
78 | if nvim_bind is not None: | |
79 | args.insert(0, nvim_bind) | |
80 | try: | |
81 | return fn(*args) | |
82 | except Exception: | |
83 | if sync: | |
84 | msg = ("error caught in request handler '{} {}':\n{}" | |
85 | .format(name, args, format_exc_skip(1))) | |
86 | raise ErrorResponse(msg) | |
87 | else: | |
88 | msg = ("error caught in async handler '{} {}'\n{}\n" | |
89 | .format(name, args, format_exc_skip(1))) | |
90 | self._on_async_err(msg + "\n") | |
91 | ||
92 | def _on_request(self, name, args): | |
93 | """Handle a msgpack-rpc request.""" | |
94 | if IS_PYTHON3: | |
95 | name = decode_if_bytes(name) | |
96 | handler = self._request_handlers.get(name, None) | |
97 | if not handler: | |
98 | msg = self._missing_handler_error(name, 'request') | |
99 | error(msg) | |
100 | raise ErrorResponse(msg) | |
101 | ||
102 | debug('calling request handler for "%s", args: "%s"', name, args) | |
103 | rv = handler(*args) | |
104 | debug("request handler for '%s %s' returns: %s", name, args, rv) | |
105 | return rv | |
106 | ||
107 | def _on_notification(self, name, args): | |
108 | """Handle a msgpack-rpc notification.""" | |
109 | if IS_PYTHON3: | |
110 | name = decode_if_bytes(name) | |
111 | handler = self._notification_handlers.get(name, None) | |
112 | if not handler: | |
113 | msg = self._missing_handler_error(name, 'notification') | |
114 | error(msg) | |
115 | self._on_async_err(msg + "\n") | |
116 | return | |
117 | ||
118 | debug('calling notification handler for "%s", args: "%s"', name, args) | |
119 | handler(*args) | |
120 | ||
121 | def _missing_handler_error(self, name, kind): | |
122 | msg = 'no {} handler registered for "{}"'.format(kind, name) | |
123 | pathmatch = re.match(r'(.+):[^:]+:[^:]+', name) | |
124 | if pathmatch: | |
125 | loader_error = self._load_errors.get(pathmatch.group(1)) | |
126 | if loader_error is not None: | |
127 | msg = msg + "\n" + loader_error | |
128 | return msg | |
129 | ||
130 | def _load(self, plugins): | |
131 | has_script = False | |
132 | for path in plugins: | |
133 | err = None | |
134 | if path in self._loaded: | |
135 | error('{} is already loaded'.format(path)) | |
136 | continue | |
137 | try: | |
138 | if path == "script_host.py": | |
139 | module = script_host | |
140 | has_script = True | |
141 | else: | |
142 | directory, name = os.path.split(os.path.splitext(path)[0]) | |
143 | file, pathname, descr = find_module(name, [directory]) | |
144 | module = imp.load_module(name, file, pathname, descr) | |
145 | handlers = [] | |
146 | self._discover_classes(module, handlers, path) | |
147 | self._discover_functions(module, handlers, path) | |
148 | if not handlers: | |
149 | error('{} exports no handlers'.format(path)) | |
150 | continue | |
151 | self._loaded[path] = {'handlers': handlers, 'module': module} | |
152 | except Exception as e: | |
153 | err = ('Encountered {} loading plugin at {}: {}\n{}' | |
154 | .format(type(e).__name__, path, e, format_exc(5))) | |
155 | error(err) | |
156 | self._load_errors[path] = err | |
157 | ||
158 | if len(plugins) == 1 and has_script: | |
159 | kind = "script" | |
160 | else: | |
161 | kind = "rplugin" | |
162 | name = "python{}-{}-host".format(sys.version_info[0], kind) | |
163 | attributes = {"license": "Apache v2", | |
164 | "website": "github.com/neovim/pynvim"} | |
165 | self.name = name | |
166 | self.nvim.api.set_client_info( | |
167 | name, VERSION.__dict__, "host", host_method_spec, | |
168 | attributes, async_=True) | |
169 | ||
170 | def _unload(self): | |
171 | for path, plugin in self._loaded.items(): | |
172 | handlers = plugin['handlers'] | |
173 | for handler in handlers: | |
174 | method_name = handler._nvim_rpc_method_name | |
175 | if hasattr(handler, '_nvim_shutdown_hook'): | |
176 | handler() | |
177 | elif handler._nvim_rpc_sync: | |
178 | del self._request_handlers[method_name] | |
179 | else: | |
180 | del self._notification_handlers[method_name] | |
181 | self._specs = {} | |
182 | self._loaded = {} | |
183 | ||
184 | def _discover_classes(self, module, handlers, plugin_path): | |
185 | for _, cls in inspect.getmembers(module, inspect.isclass): | |
186 | if getattr(cls, '_nvim_plugin', False): | |
187 | # create an instance of the plugin and pass the nvim object | |
188 | plugin = cls(self._configure_nvim_for(cls)) | |
189 | # discover handlers in the plugin instance | |
190 | self._discover_functions(plugin, handlers, plugin_path) | |
191 | ||
192 | def _discover_functions(self, obj, handlers, plugin_path): | |
193 | def predicate(o): | |
194 | return hasattr(o, '_nvim_rpc_method_name') | |
195 | ||
196 | specs = [] | |
197 | objdecode = getattr(obj, '_nvim_decode', self._decode_default) | |
198 | for _, fn in inspect.getmembers(obj, predicate): | |
199 | sync = fn._nvim_rpc_sync | |
200 | decode = getattr(fn, '_nvim_decode', objdecode) | |
201 | nvim_bind = None | |
202 | if fn._nvim_bind: | |
203 | nvim_bind = self._configure_nvim_for(fn) | |
204 | ||
205 | method = fn._nvim_rpc_method_name | |
206 | if fn._nvim_prefix_plugin_path: | |
207 | method = '{}:{}'.format(plugin_path, method) | |
208 | ||
209 | fn_wrapped = partial(self._wrap_function, fn, | |
210 | sync, decode, nvim_bind, method) | |
211 | self._copy_attributes(fn, fn_wrapped) | |
212 | # register in the rpc handler dict | |
213 | if sync: | |
214 | if method in self._request_handlers: | |
215 | raise Exception(('Request handler for "{}" is ' | |
216 | + 'already registered').format(method)) | |
217 | self._request_handlers[method] = fn_wrapped | |
218 | else: | |
219 | if method in self._notification_handlers: | |
220 | raise Exception(('Notification handler for "{}" is ' | |
221 | + 'already registered').format(method)) | |
222 | self._notification_handlers[method] = fn_wrapped | |
223 | if hasattr(fn, '_nvim_rpc_spec'): | |
224 | specs.append(fn._nvim_rpc_spec) | |
225 | handlers.append(fn_wrapped) | |
226 | if specs: | |
227 | self._specs[plugin_path] = specs | |
228 | ||
229 | def _copy_attributes(self, fn, fn2): | |
230 | # Copy _nvim_* attributes from the original function | |
231 | for attr in dir(fn): | |
232 | if attr.startswith('_nvim_'): | |
233 | setattr(fn2, attr, getattr(fn, attr)) | |
234 | ||
235 | def _on_specs_request(self, path): | |
236 | if IS_PYTHON3: | |
237 | path = decode_if_bytes(path) | |
238 | if path in self._load_errors: | |
239 | self.nvim.out_write(self._load_errors[path] + '\n') | |
240 | return self._specs.get(path, 0) | |
241 | ||
242 | def _configure_nvim_for(self, obj): | |
243 | # Configure a nvim instance for obj (checks encoding configuration) | |
244 | nvim = self.nvim | |
245 | decode = getattr(obj, '_nvim_decode', self._decode_default) | |
246 | if decode: | |
247 | nvim = nvim.with_decode(decode) | |
248 | return nvim |
0 | """Legacy python/python3-vim emulation.""" | |
1 | import imp | |
2 | import io | |
3 | import logging | |
4 | import os | |
5 | import sys | |
6 | ||
7 | from .decorators import plugin, rpc_export | |
8 | from ..api import Nvim, walk | |
9 | from ..compat import IS_PYTHON3 | |
10 | from ..msgpack_rpc import ErrorResponse | |
11 | from ..util import format_exc_skip | |
12 | ||
13 | __all__ = ('ScriptHost',) | |
14 | ||
15 | ||
16 | logger = logging.getLogger(__name__) | |
17 | debug, info, warn = (logger.debug, logger.info, logger.warn,) | |
18 | ||
19 | if IS_PYTHON3: | |
20 | basestring = str | |
21 | ||
22 | if sys.version_info >= (3, 4): | |
23 | from importlib.machinery import PathFinder | |
24 | ||
25 | PYTHON_SUBDIR = 'python3' | |
26 | else: | |
27 | PYTHON_SUBDIR = 'python2' | |
28 | ||
29 | ||
30 | @plugin | |
31 | class ScriptHost(object): | |
32 | ||
33 | """Provides an environment for running python plugins created for Vim.""" | |
34 | ||
35 | def __init__(self, nvim): | |
36 | """Initialize the legacy python-vim environment.""" | |
37 | self.setup(nvim) | |
38 | # context where all code will run | |
39 | self.module = imp.new_module('__main__') | |
40 | nvim.script_context = self.module | |
41 | # it seems some plugins assume 'sys' is already imported, so do it now | |
42 | exec('import sys', self.module.__dict__) | |
43 | self.legacy_vim = LegacyVim.from_nvim(nvim) | |
44 | sys.modules['vim'] = self.legacy_vim | |
45 | ||
46 | # Handle DirChanged. #296 | |
47 | nvim.command( | |
48 | 'au DirChanged * call rpcnotify({}, "python_chdir", v:event.cwd)' | |
49 | .format(nvim.channel_id), async_=True) | |
50 | # XXX: Avoid race condition. | |
51 | # https://github.com/neovim/pynvim/pull/296#issuecomment-358970531 | |
52 | # TODO(bfredl): when host initialization has been refactored, | |
53 | # to make __init__ safe again, the following should work: | |
54 | # os.chdir(nvim.eval('getcwd()', async_=False)) | |
55 | nvim.command('call rpcnotify({}, "python_chdir", getcwd())' | |
56 | .format(nvim.channel_id), async_=True) | |
57 | ||
58 | def setup(self, nvim): | |
59 | """Setup import hooks and global streams. | |
60 | ||
61 | This will add import hooks for importing modules from runtime | |
62 | directories and patch the sys module so 'print' calls will be | |
63 | forwarded to Nvim. | |
64 | """ | |
65 | self.nvim = nvim | |
66 | info('install import hook/path') | |
67 | self.hook = path_hook(nvim) | |
68 | sys.path_hooks.append(self.hook) | |
69 | nvim.VIM_SPECIAL_PATH = '_vim_path_' | |
70 | sys.path.append(nvim.VIM_SPECIAL_PATH) | |
71 | info('redirect sys.stdout and sys.stderr') | |
72 | self.saved_stdout = sys.stdout | |
73 | self.saved_stderr = sys.stderr | |
74 | sys.stdout = RedirectStream(lambda data: nvim.out_write(data)) | |
75 | sys.stderr = RedirectStream(lambda data: nvim.err_write(data)) | |
76 | ||
77 | def teardown(self): | |
78 | """Restore state modified from the `setup` call.""" | |
79 | nvim = self.nvim | |
80 | info('uninstall import hook/path') | |
81 | sys.path.remove(nvim.VIM_SPECIAL_PATH) | |
82 | sys.path_hooks.remove(self.hook) | |
83 | info('restore sys.stdout and sys.stderr') | |
84 | sys.stdout = self.saved_stdout | |
85 | sys.stderr = self.saved_stderr | |
86 | ||
87 | @rpc_export('python_execute', sync=True) | |
88 | def python_execute(self, script, range_start, range_stop): | |
89 | """Handle the `python` ex command.""" | |
90 | self._set_current_range(range_start, range_stop) | |
91 | try: | |
92 | exec(script, self.module.__dict__) | |
93 | except Exception: | |
94 | raise ErrorResponse(format_exc_skip(1)) | |
95 | ||
96 | @rpc_export('python_execute_file', sync=True) | |
97 | def python_execute_file(self, file_path, range_start, range_stop): | |
98 | """Handle the `pyfile` ex command.""" | |
99 | self._set_current_range(range_start, range_stop) | |
100 | with open(file_path) as f: | |
101 | script = compile(f.read(), file_path, 'exec') | |
102 | try: | |
103 | exec(script, self.module.__dict__) | |
104 | except Exception: | |
105 | raise ErrorResponse(format_exc_skip(1)) | |
106 | ||
107 | @rpc_export('python_do_range', sync=True) | |
108 | def python_do_range(self, start, stop, code): | |
109 | """Handle the `pydo` ex command.""" | |
110 | self._set_current_range(start, stop) | |
111 | nvim = self.nvim | |
112 | start -= 1 | |
113 | fname = '_vim_pydo' | |
114 | ||
115 | # define the function | |
116 | function_def = 'def %s(line, linenr):\n %s' % (fname, code,) | |
117 | exec(function_def, self.module.__dict__) | |
118 | # get the function | |
119 | function = self.module.__dict__[fname] | |
120 | while start < stop: | |
121 | # Process batches of 5000 to avoid the overhead of making multiple | |
122 | # API calls for every line. Assuming an average line length of 100 | |
123 | # bytes, approximately 488 kilobytes will be transferred per batch, | |
124 | # which can be done very quickly in a single API call. | |
125 | sstart = start | |
126 | sstop = min(start + 5000, stop) | |
127 | lines = nvim.current.buffer.api.get_lines(sstart, sstop, True) | |
128 | ||
129 | exception = None | |
130 | newlines = [] | |
131 | linenr = sstart + 1 | |
132 | for i, line in enumerate(lines): | |
133 | result = function(line, linenr) | |
134 | if result is None: | |
135 | # Update earlier lines, and skip to the next | |
136 | if newlines: | |
137 | end = sstart + len(newlines) - 1 | |
138 | nvim.current.buffer.api.set_lines(sstart, end, | |
139 | True, newlines) | |
140 | sstart += len(newlines) + 1 | |
141 | newlines = [] | |
142 | pass | |
143 | elif isinstance(result, basestring): | |
144 | newlines.append(result) | |
145 | else: | |
146 | exception = TypeError('pydo should return a string ' | |
147 | + 'or None, found %s instead' | |
148 | % result.__class__.__name__) | |
149 | break | |
150 | linenr += 1 | |
151 | ||
152 | start = sstop | |
153 | if newlines: | |
154 | end = sstart + len(newlines) | |
155 | nvim.current.buffer.api.set_lines(sstart, end, True, newlines) | |
156 | if exception: | |
157 | raise exception | |
158 | # delete the function | |
159 | del self.module.__dict__[fname] | |
160 | ||
161 | @rpc_export('python_eval', sync=True) | |
162 | def python_eval(self, expr): | |
163 | """Handle the `pyeval` vim function.""" | |
164 | return eval(expr, self.module.__dict__) | |
165 | ||
166 | @rpc_export('python_chdir', sync=False) | |
167 | def python_chdir(self, cwd): | |
168 | """Handle working directory changes.""" | |
169 | os.chdir(cwd) | |
170 | ||
171 | def _set_current_range(self, start, stop): | |
172 | current = self.legacy_vim.current | |
173 | current.range = current.buffer.range(start, stop) | |
174 | ||
175 | ||
176 | class RedirectStream(io.IOBase): | |
177 | def __init__(self, redirect_handler): | |
178 | self.redirect_handler = redirect_handler | |
179 | ||
180 | def write(self, data): | |
181 | self.redirect_handler(data) | |
182 | ||
183 | def writelines(self, seq): | |
184 | self.redirect_handler('\n'.join(seq)) | |
185 | ||
186 | ||
187 | if IS_PYTHON3: | |
188 | num_types = (int, float) | |
189 | else: | |
190 | num_types = (int, long, float) # noqa: F821 | |
191 | ||
192 | ||
193 | def num_to_str(obj): | |
194 | if isinstance(obj, num_types): | |
195 | return str(obj) | |
196 | else: | |
197 | return obj | |
198 | ||
199 | ||
200 | class LegacyVim(Nvim): | |
201 | def eval(self, expr): | |
202 | obj = self.request("vim_eval", expr) | |
203 | return walk(num_to_str, obj) | |
204 | ||
205 | ||
206 | # Copied/adapted from :help if_pyth. | |
207 | def path_hook(nvim): | |
208 | def _get_paths(): | |
209 | if nvim._thread_invalid(): | |
210 | return [] | |
211 | return discover_runtime_directories(nvim) | |
212 | ||
213 | def _find_module(fullname, oldtail, path): | |
214 | idx = oldtail.find('.') | |
215 | if idx > 0: | |
216 | name = oldtail[:idx] | |
217 | tail = oldtail[idx + 1:] | |
218 | fmr = imp.find_module(name, path) | |
219 | module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr) | |
220 | return _find_module(fullname, tail, module.__path__) | |
221 | else: | |
222 | return imp.find_module(fullname, path) | |
223 | ||
224 | class VimModuleLoader(object): | |
225 | def __init__(self, module): | |
226 | self.module = module | |
227 | ||
228 | def load_module(self, fullname, path=None): | |
229 | # Check sys.modules, required for reload (see PEP302). | |
230 | try: | |
231 | return sys.modules[fullname] | |
232 | except KeyError: | |
233 | pass | |
234 | return imp.load_module(fullname, *self.module) | |
235 | ||
236 | class VimPathFinder(object): | |
237 | @staticmethod | |
238 | def find_module(fullname, path=None): | |
239 | """Method for Python 2.7 and 3.3.""" | |
240 | try: | |
241 | return VimModuleLoader( | |
242 | _find_module(fullname, fullname, path or _get_paths())) | |
243 | except ImportError: | |
244 | return None | |
245 | ||
246 | @staticmethod | |
247 | def find_spec(fullname, target=None): | |
248 | """Method for Python 3.4+.""" | |
249 | return PathFinder.find_spec(fullname, _get_paths(), target) | |
250 | ||
251 | def hook(path): | |
252 | if path == nvim.VIM_SPECIAL_PATH: | |
253 | return VimPathFinder | |
254 | else: | |
255 | raise ImportError | |
256 | ||
257 | return hook | |
258 | ||
259 | ||
260 | def discover_runtime_directories(nvim): | |
261 | rv = [] | |
262 | for rtp in nvim.list_runtime_paths(): | |
263 | if not os.path.exists(rtp): | |
264 | continue | |
265 | for subdir in ['pythonx', PYTHON_SUBDIR]: | |
266 | path = os.path.join(rtp, subdir) | |
267 | if os.path.exists(path): | |
268 | rv.append(path) | |
269 | return rv |
0 | """Shared utility functions.""" | |
1 | ||
2 | import sys | |
3 | from traceback import format_exception | |
4 | ||
5 | ||
6 | def format_exc_skip(skip, limit=None): | |
7 | """Like traceback.format_exc but allow skipping the first frames.""" | |
8 | etype, val, tb = sys.exc_info() | |
9 | for i in range(skip): | |
10 | tb = tb.tb_next | |
11 | return (''.join(format_exception(etype, val, tb, limit))).rstrip() | |
12 | ||
13 | ||
14 | # Taken from SimpleNamespace in python 3 | |
15 | class Version: | |
16 | ||
17 | """Helper class for version info.""" | |
18 | ||
19 | def __init__(self, **kwargs): | |
20 | """Create the Version object.""" | |
21 | self.__dict__.update(kwargs) | |
22 | ||
23 | def __repr__(self): | |
24 | """Return str representation of the Version.""" | |
25 | keys = sorted(self.__dict__) | |
26 | items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) | |
27 | return "{}({})".format(type(self).__name__, ", ".join(items)) | |
28 | ||
29 | def __eq__(self, other): | |
30 | """Check if version is same as other.""" | |
31 | return self.__dict__ == other.__dict__ | |
32 | ||
33 | ||
34 | VERSION = Version(major=0, minor=3, patch=2, prerelease='') |
0 | Metadata-Version: 2.1 | |
1 | Name: pynvim | |
2 | Version: 0.3.2 | |
3 | Summary: Python client to neovim | |
4 | Home-page: http://github.com/neovim/python-client | |
5 | Author: Thiago de Arruda | |
6 | Author-email: tpadilha84@gmail.com | |
7 | License: Apache | |
8 | Download-URL: https://github.com/neovim/python-client/archive/0.3.2.tar.gz | |
9 | Description: UNKNOWN | |
10 | Platform: UNKNOWN | |
11 | Provides-Extra: test | |
12 | Provides-Extra: pyuv |
0 | LICENSE | |
1 | MANIFEST.in | |
2 | README.md | |
3 | setup.cfg | |
4 | setup.py | |
5 | neovim/__init__.py | |
6 | neovim/api/__init__.py | |
7 | pynvim/__init__.py | |
8 | pynvim/compat.py | |
9 | pynvim/util.py | |
10 | pynvim.egg-info/PKG-INFO | |
11 | pynvim.egg-info/SOURCES.txt | |
12 | pynvim.egg-info/dependency_links.txt | |
13 | pynvim.egg-info/not-zip-safe | |
14 | pynvim.egg-info/requires.txt | |
15 | pynvim.egg-info/top_level.txt | |
16 | pynvim/api/__init__.py | |
17 | pynvim/api/buffer.py | |
18 | pynvim/api/common.py | |
19 | pynvim/api/nvim.py | |
20 | pynvim/api/tabpage.py | |
21 | pynvim/api/window.py | |
22 | pynvim/msgpack_rpc/__init__.py | |
23 | pynvim/msgpack_rpc/async_session.py | |
24 | pynvim/msgpack_rpc/msgpack_stream.py | |
25 | pynvim/msgpack_rpc/session.py | |
26 | pynvim/msgpack_rpc/event_loop/__init__.py | |
27 | pynvim/msgpack_rpc/event_loop/asyncio.py | |
28 | pynvim/msgpack_rpc/event_loop/base.py | |
29 | pynvim/msgpack_rpc/event_loop/uv.py | |
30 | pynvim/plugin/__init__.py | |
31 | pynvim/plugin/decorators.py | |
32 | pynvim/plugin/host.py | |
33 | pynvim/plugin/script_host.py | |
34 | test/test_buffer.py | |
35 | test/test_client_rpc.py | |
36 | test/test_concurrency.py | |
37 | test/test_decorators.py | |
38 | test/test_events.py | |
39 | test/test_host.py | |
40 | test/test_tabpage.py | |
41 | test/test_vim.py | |
42 | test/test_window.py⏎ |
0 | 0 | [flake8] |
1 | ignore = D211,E731,D401 | |
1 | ignore = D211,E731,D401,W503 | |
2 | 2 | |
3 | 3 | [tool:pytest] |
4 | 4 | testpaths = test |
27 | 27 | # pypy already includes an implementation of the greenlet module |
28 | 28 | install_requires.append('greenlet') |
29 | 29 | |
30 | setup(name='neovim', | |
31 | version='0.3.0', | |
30 | setup(name='pynvim', | |
31 | version='0.3.2', | |
32 | 32 | description='Python client to neovim', |
33 | 33 | url='http://github.com/neovim/python-client', |
34 | download_url='https://github.com/neovim/python-client/archive/0.3.0.tar.gz', | |
34 | download_url='https://github.com/neovim/python-client/archive/0.3.2.tar.gz', | |
35 | 35 | author='Thiago de Arruda', |
36 | 36 | author_email='tpadilha84@gmail.com', |
37 | 37 | license='Apache', |
38 | packages=['neovim', 'neovim.api', 'neovim.msgpack_rpc', | |
39 | 'neovim.msgpack_rpc.event_loop', 'neovim.plugin'], | |
38 | packages=['pynvim', 'pynvim.api', 'pynvim.msgpack_rpc', | |
39 | 'pynvim.msgpack_rpc.event_loop', 'pynvim.plugin', | |
40 | 'neovim', 'neovim.api'], | |
40 | 41 | install_requires=install_requires, |
41 | 42 | tests_require=tests_require, |
42 | 43 | extras_require=extras_require, |
0 | 0 | import os |
1 | 1 | |
2 | from neovim.compat import IS_PYTHON3 | |
2 | from pynvim.compat import IS_PYTHON3 | |
3 | 3 | |
4 | 4 | |
5 | 5 | def test_repr(vim): |
0 | from neovim.plugin.decorators import command | |
0 | from pynvim.plugin.decorators import command | |
1 | 1 | |
2 | 2 | |
3 | 3 | def test_command_count(): |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | |
2 | from neovim.plugin.host import Host, host_method_spec | |
2 | from pynvim.plugin.host import Host, host_method_spec | |
3 | 3 | |
4 | 4 | def test_host_method_spec(vim): |
5 | 5 | h = Host(vim) |
71 | 71 | assert vim.current.line == '' |
72 | 72 | vim.current.line = 'abc' |
73 | 73 | assert vim.current.line == 'abc' |
74 | ||
75 | ||
76 | def test_current_line_delete(vim): | |
77 | vim.current.buffer[:] = ['one', 'two'] | |
78 | assert len(vim.current.buffer[:]) == 2 | |
79 | del vim.current.line | |
80 | assert len(vim.current.buffer[:]) == 1 and vim.current.buffer[0] == 'two' | |
81 | del vim.current.line | |
82 | assert len(vim.current.buffer[:]) == 1 and not vim.current.buffer[0] | |
74 | 83 | |
75 | 84 | |
76 | 85 | def test_vars(vim): |