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