Skip to content

Commit 2cebda2

Browse files
committed
Merge pull request #180 from bfredl/script_host
Script host
2 parents 720b3d5 + 72b6ba7 commit 2cebda2

File tree

3 files changed

+253
-2
lines changed

3 files changed

+253
-2
lines changed

neovim/plugin/host.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,11 @@ def _load(self, plugins):
9090
if path in self._loaded:
9191
error('{0} is already loaded'.format(path))
9292
continue
93-
directory, name = os.path.split(os.path.splitext(path)[0])
93+
if path == "script_host.py":
94+
directory = os.path.dirname(__file__)
95+
name = "script_host"
96+
else:
97+
directory, name = os.path.split(os.path.splitext(path)[0])
9498
file, pathname, description = find_module(name, [directory])
9599
handlers = []
96100
try:

neovim/plugin/script_host.py

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

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[flake8]
2-
ignore = D211,E731
2+
ignore = D211,E731,F821

0 commit comments

Comments
 (0)