1
|
|
|
"""Legacy python/python3-vim emulation.""" |
2
|
6 |
|
import imp |
3
|
6 |
|
import io |
4
|
6 |
|
import logging |
5
|
6 |
|
import os |
6
|
6 |
|
import sys |
7
|
|
|
|
8
|
6 |
|
from .decorators import plugin, rpc_export |
9
|
6 |
|
from ..api import Nvim, walk |
10
|
6 |
|
from ..msgpack_rpc import ErrorResponse |
11
|
6 |
|
from ..util import format_exc_skip |
12
|
|
|
|
13
|
6 |
|
__all__ = ('ScriptHost',) |
14
|
|
|
|
15
|
|
|
|
16
|
6 |
|
logger = logging.getLogger(__name__) |
17
|
6 |
|
debug, info, warn = (logger.debug, logger.info, logger.warn,) |
18
|
|
|
|
19
|
6 |
|
IS_PYTHON3 = sys.version_info >= (3, 0) |
20
|
|
|
|
21
|
6 |
|
if IS_PYTHON3: |
22
|
3 |
|
basestring = str |
23
|
|
|
|
24
|
3 |
|
if sys.version_info >= (3, 4): |
25
|
2 |
|
from importlib.machinery import PathFinder |
26
|
|
|
|
27
|
|
|
|
28
|
6 |
|
@plugin |
29
|
6 |
|
class ScriptHost(object): |
30
|
|
|
|
31
|
|
|
"""Provides an environment for running python plugins created for Vim.""" |
32
|
|
|
|
33
|
6 |
|
def __init__(self, nvim): |
34
|
|
|
"""Initialize the legacy python-vim environment.""" |
35
|
|
|
self.setup(nvim) |
36
|
|
|
# context where all code will run |
37
|
|
|
self.module = imp.new_module('__main__') |
38
|
|
|
nvim.script_context = self.module |
39
|
|
|
# it seems some plugins assume 'sys' is already imported, so do it now |
40
|
|
|
exec('import sys', self.module.__dict__) |
|
|
|
|
41
|
|
|
self.legacy_vim = LegacyVim.from_nvim(nvim) |
42
|
|
|
sys.modules['vim'] = self.legacy_vim |
43
|
|
|
|
44
|
6 |
|
def setup(self, nvim): |
45
|
|
|
"""Setup import hooks and global streams. |
46
|
|
|
|
47
|
|
|
This will add import hooks for importing modules from runtime |
48
|
|
|
directories and patch the sys module so 'print' calls will be |
49
|
|
|
forwarded to Nvim. |
50
|
|
|
""" |
51
|
|
|
self.nvim = nvim |
52
|
|
|
info('install import hook/path') |
53
|
|
|
self.hook = path_hook(nvim) |
54
|
|
|
sys.path_hooks.append(self.hook) |
55
|
|
|
nvim.VIM_SPECIAL_PATH = '_vim_path_' |
56
|
|
|
sys.path.append(nvim.VIM_SPECIAL_PATH) |
57
|
|
|
info('redirect sys.stdout and sys.stderr') |
58
|
|
|
self.saved_stdout = sys.stdout |
59
|
|
|
self.saved_stderr = sys.stderr |
60
|
|
|
sys.stdout = RedirectStream(lambda data: nvim.out_write(data)) |
|
|
|
|
61
|
|
|
sys.stderr = RedirectStream(lambda data: nvim.err_write(data)) |
|
|
|
|
62
|
|
|
|
63
|
6 |
|
def teardown(self): |
64
|
|
|
"""Restore state modified from the `setup` call.""" |
65
|
|
|
nvim = self.nvim |
66
|
|
|
info('uninstall import hook/path') |
67
|
|
|
sys.path.remove(nvim.VIM_SPECIAL_PATH) |
68
|
|
|
sys.path_hooks.remove(self.hook) |
69
|
|
|
info('restore sys.stdout and sys.stderr') |
70
|
|
|
sys.stdout = self.saved_stdout |
71
|
|
|
sys.stderr = self.saved_stderr |
72
|
|
|
|
73
|
6 |
|
@rpc_export('python_execute', sync=True) |
74
|
|
|
def python_execute(self, script, range_start, range_stop): |
75
|
|
|
"""Handle the `python` ex command.""" |
76
|
|
|
self._set_current_range(range_start, range_stop) |
77
|
|
|
try: |
78
|
|
|
exec(script, self.module.__dict__) |
|
|
|
|
79
|
|
|
except Exception: |
80
|
|
|
raise ErrorResponse(format_exc_skip(1)) |
81
|
|
|
|
82
|
6 |
|
@rpc_export('python_execute_file', sync=True) |
83
|
|
|
def python_execute_file(self, file_path, range_start, range_stop): |
84
|
|
|
"""Handle the `pyfile` ex command.""" |
85
|
|
|
self._set_current_range(range_start, range_stop) |
86
|
|
|
with open(file_path) as f: |
87
|
|
|
script = compile(f.read(), file_path, 'exec') |
88
|
|
|
try: |
89
|
|
|
exec(script, self.module.__dict__) |
|
|
|
|
90
|
|
|
except Exception: |
91
|
|
|
raise ErrorResponse(format_exc_skip(1)) |
92
|
|
|
|
93
|
6 |
|
@rpc_export('python_do_range', sync=True) |
94
|
|
|
def python_do_range(self, start, stop, code): |
95
|
|
|
"""Handle the `pydo` ex command.""" |
96
|
|
|
self._set_current_range(start, stop) |
97
|
|
|
nvim = self.nvim |
98
|
|
|
start -= 1 |
99
|
|
|
stop -= 1 |
100
|
|
|
fname = '_vim_pydo' |
101
|
|
|
|
102
|
|
|
# define the function |
103
|
|
|
function_def = 'def %s(line, linenr):\n %s' % (fname, code,) |
104
|
|
|
exec(function_def, self.module.__dict__) |
|
|
|
|
105
|
|
|
# get the function |
106
|
|
|
function = self.module.__dict__[fname] |
107
|
|
|
while start <= stop: |
108
|
|
|
# Process batches of 5000 to avoid the overhead of making multiple |
109
|
|
|
# API calls for every line. Assuming an average line length of 100 |
110
|
|
|
# bytes, approximately 488 kilobytes will be transferred per batch, |
111
|
|
|
# which can be done very quickly in a single API call. |
112
|
|
|
sstart = start |
113
|
|
|
sstop = min(start + 5000, stop) |
114
|
|
|
lines = nvim.current.buffer.get_line_slice(sstart, sstop, True, |
115
|
|
|
True) |
116
|
|
|
|
117
|
|
|
exception = None |
118
|
|
|
newlines = [] |
119
|
|
|
linenr = sstart + 1 |
120
|
|
|
for i, line in enumerate(lines): |
|
|
|
|
121
|
|
|
result = function(line, linenr) |
122
|
|
|
if result is None: |
123
|
|
|
# Update earlier lines, and skip to the next |
124
|
|
|
if newlines: |
125
|
|
|
end = sstart + len(newlines) - 1 |
126
|
|
|
nvim.current.buffer.set_line_slice(sstart, end, |
127
|
|
|
True, True, |
128
|
|
|
newlines) |
129
|
|
|
sstart += len(newlines) + 1 |
130
|
|
|
newlines = [] |
131
|
|
|
pass |
|
|
|
|
132
|
|
|
elif isinstance(result, basestring): |
133
|
|
|
newlines.append(result) |
134
|
|
|
else: |
135
|
|
|
exception = TypeError('pydo should return a string ' + |
136
|
|
|
'or None, found %s instead' |
137
|
|
|
% result.__class__.__name__) |
138
|
|
|
break |
139
|
|
|
linenr += 1 |
140
|
|
|
|
141
|
|
|
start = sstop + 1 |
142
|
|
|
if newlines: |
143
|
|
|
end = sstart + len(newlines) - 1 |
144
|
|
|
nvim.current.buffer.set_line_slice(sstart, end, True, True, |
145
|
|
|
newlines) |
146
|
|
|
if exception: |
147
|
|
|
raise exception |
|
|
|
|
148
|
|
|
# delete the function |
149
|
|
|
del self.module.__dict__[fname] |
150
|
|
|
|
151
|
6 |
|
@rpc_export('python_eval', sync=True) |
152
|
|
|
def python_eval(self, expr): |
153
|
|
|
"""Handle the `pyeval` vim function.""" |
154
|
|
|
return eval(expr, self.module.__dict__) |
|
|
|
|
155
|
|
|
|
156
|
6 |
|
def _set_current_range(self, start, stop): |
157
|
|
|
current = self.legacy_vim.current |
158
|
|
|
current.range = current.buffer.range(start, stop) |
159
|
|
|
|
160
|
|
|
|
161
|
6 |
|
class RedirectStream(io.IOBase): |
162
|
6 |
|
def __init__(self, redirect_handler): |
163
|
|
|
self.redirect_handler = redirect_handler |
164
|
|
|
|
165
|
6 |
|
def write(self, data): |
166
|
|
|
self.redirect_handler(data) |
167
|
|
|
|
168
|
6 |
|
def writelines(self, seq): |
169
|
|
|
self.redirect_handler('\n'.join(seq)) |
170
|
|
|
|
171
|
|
|
|
172
|
6 |
|
if IS_PYTHON3: |
173
|
3 |
|
num_types = (int, float) |
174
|
|
|
else: |
175
|
3 |
|
num_types = (int, long, float) |
|
|
|
|
176
|
|
|
|
177
|
|
|
|
178
|
6 |
|
def num_to_str(obj): |
179
|
|
|
if isinstance(obj, num_types): |
180
|
|
|
return str(obj) |
181
|
|
|
else: |
182
|
|
|
return obj |
183
|
|
|
|
184
|
|
|
|
185
|
6 |
|
class LegacyVim(Nvim): |
186
|
6 |
|
def eval(self, expr): |
187
|
|
|
obj = self.request("vim_eval", expr) |
188
|
|
|
return walk(num_to_str, obj) |
189
|
|
|
|
190
|
|
|
|
191
|
|
|
# This was copied/adapted from nvim-python help |
192
|
6 |
|
def path_hook(nvim): |
193
|
|
|
def _get_paths(): |
194
|
|
|
return discover_runtime_directories(nvim) |
195
|
|
|
|
196
|
|
|
def _find_module(fullname, oldtail, path): |
197
|
|
|
idx = oldtail.find('.') |
198
|
|
|
if idx > 0: |
199
|
|
|
name = oldtail[:idx] |
200
|
|
|
tail = oldtail[idx + 1:] |
201
|
|
|
fmr = imp.find_module(name, path) |
202
|
|
|
module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr) |
203
|
|
|
return _find_module(fullname, tail, module.__path__) |
|
|
|
|
204
|
|
|
else: |
205
|
|
|
return imp.find_module(fullname, path) |
206
|
|
|
|
207
|
|
|
class VimModuleLoader(object): |
208
|
|
|
def __init__(self, module): |
209
|
|
|
self.module = module |
210
|
|
|
|
211
|
|
|
def load_module(self, fullname, path=None): |
|
|
|
|
212
|
|
|
# Check sys.modules, required for reload (see PEP302). |
213
|
|
|
if fullname in sys.modules: |
214
|
|
|
return sys.modules[fullname] |
215
|
|
|
return imp.load_module(fullname, *self.module) |
216
|
|
|
|
217
|
|
|
class VimPathFinder(object): |
218
|
|
|
@staticmethod |
219
|
|
|
def find_module(fullname, path=None): |
220
|
|
|
"""Method for Python 2.7 and 3.3.""" |
221
|
|
|
try: |
222
|
|
|
return VimModuleLoader( |
223
|
|
|
_find_module(fullname, fullname, path or _get_paths())) |
224
|
|
|
except ImportError: |
225
|
|
|
return None |
226
|
|
|
|
227
|
|
|
@staticmethod |
228
|
|
|
def find_spec(fullname, path=None, target=None): |
229
|
|
|
"""Method for Python 3.4+.""" |
230
|
|
|
return PathFinder.find_spec(fullname, path or _get_paths(), target) |
231
|
|
|
|
232
|
|
|
def hook(path): |
233
|
|
|
if path == nvim.VIM_SPECIAL_PATH: |
234
|
|
|
return VimPathFinder |
235
|
|
|
else: |
236
|
|
|
raise ImportError |
237
|
|
|
|
238
|
|
|
return hook |
239
|
|
|
|
240
|
|
|
|
241
|
6 |
|
def discover_runtime_directories(nvim): |
242
|
|
|
rv = [] |
243
|
|
|
for path in nvim.list_runtime_paths(): |
244
|
|
|
if not os.path.exists(path): |
245
|
|
|
continue |
246
|
|
|
path1 = os.path.join(path, 'pythonx') |
247
|
|
|
if IS_PYTHON3: |
248
|
|
|
path2 = os.path.join(path, 'python3') |
249
|
|
|
else: |
250
|
|
|
path2 = os.path.join(path, 'python2') |
251
|
|
|
if os.path.exists(path1): |
252
|
|
|
rv.append(path1) |
253
|
|
|
if os.path.exists(path2): |
254
|
|
|
rv.append(path2) |
255
|
|
|
return rv |
256
|
|
|
|
Execution of dynamic code might introduce security vulnerabilities. It is generally recommended to use this feature with care and only when necessary.