1 """Legacy python/python3-vim emulation."""
7 from types import ModuleType
9 from pynvim.api import Nvim, walk
10 from pynvim.compat import IS_PYTHON3
11 from pynvim.msgpack_rpc import ErrorResponse
12 from pynvim.plugin.decorators import plugin, rpc_export
13 from pynvim.util import format_exc_skip
15 __all__ = ('ScriptHost',)
18 logger = logging.getLogger(__name__)
19 debug, info, warn = (logger.debug, logger.info, logger.warn,)
24 if sys.version_info >= (3, 4):
25 from importlib.machinery import PathFinder
27 PYTHON_SUBDIR = 'python3'
29 PYTHON_SUBDIR = 'python2'
33 class ScriptHost(object):
35 """Provides an environment for running python plugins created for Vim."""
37 def __init__(self, nvim):
38 """Initialize the legacy python-vim environment."""
40 # context where all code will run
41 self.module = ModuleType('__main__')
42 nvim.script_context = self.module
43 # it seems some plugins assume 'sys' is already imported, so do it now
44 exec('import sys', self.module.__dict__)
45 self.legacy_vim = LegacyVim.from_nvim(nvim)
46 sys.modules['vim'] = self.legacy_vim
47 # mimic Vim by importing vim module by default.
48 exec('import vim', self.module.__dict__)
49 # Handle DirChanged. #296
51 'au DirChanged * call rpcnotify({}, "python_chdir", v:event.cwd)'
52 .format(nvim.channel_id), async_=True)
53 # XXX: Avoid race condition.
54 # https://github.com/neovim/pynvim/pull/296#issuecomment-358970531
55 # TODO(bfredl): when host initialization has been refactored,
56 # to make __init__ safe again, the following should work:
57 # os.chdir(nvim.eval('getcwd()', async_=False))
58 nvim.command('call rpcnotify({}, "python_chdir", getcwd())'
59 .format(nvim.channel_id), async_=True)
61 def setup(self, nvim):
62 """Setup import hooks and global streams.
64 This will add import hooks for importing modules from runtime
65 directories and patch the sys module so 'print' calls will be
69 pass # replaces next logging statement
70 #info('install import hook/path')
71 self.hook = path_hook(nvim)
72 sys.path_hooks.append(self.hook)
73 nvim.VIM_SPECIAL_PATH = '_vim_path_'
74 sys.path.append(nvim.VIM_SPECIAL_PATH)
75 pass # replaces next logging statement
76 #info('redirect sys.stdout and sys.stderr')
77 self.saved_stdout = sys.stdout
78 self.saved_stderr = sys.stderr
79 sys.stdout = RedirectStream(lambda data: nvim.out_write(data))
80 sys.stderr = RedirectStream(lambda data: nvim.err_write(data))
83 """Restore state modified from the `setup` call."""
85 pass # replaces next logging statement
86 #info('uninstall import hook/path')
87 sys.path.remove(nvim.VIM_SPECIAL_PATH)
88 sys.path_hooks.remove(self.hook)
89 pass # replaces next logging statement
90 #info('restore sys.stdout and sys.stderr')
91 sys.stdout = self.saved_stdout
92 sys.stderr = self.saved_stderr
94 @rpc_export('python_execute', sync=True)
95 def python_execute(self, script, range_start, range_stop):
96 """Handle the `python` ex command."""
97 self._set_current_range(range_start, range_stop)
99 exec(script, self.module.__dict__)
101 raise ErrorResponse(format_exc_skip(1))
103 @rpc_export('python_execute_file', sync=True)
104 def python_execute_file(self, file_path, range_start, range_stop):
105 """Handle the `pyfile` ex command."""
106 self._set_current_range(range_start, range_stop)
107 with open(file_path) as f:
108 script = compile(f.read(), file_path, 'exec')
110 exec(script, self.module.__dict__)
112 raise ErrorResponse(format_exc_skip(1))
114 @rpc_export('python_do_range', sync=True)
115 def python_do_range(self, start, stop, code):
116 """Handle the `pydo` ex command."""
117 self._set_current_range(start, stop)
122 # define the function
123 function_def = 'def %s(line, linenr):\n %s' % (fname, code,)
124 exec(function_def, self.module.__dict__)
126 function = self.module.__dict__[fname]
128 # Process batches of 5000 to avoid the overhead of making multiple
129 # API calls for every line. Assuming an average line length of 100
130 # bytes, approximately 488 kilobytes will be transferred per batch,
131 # which can be done very quickly in a single API call.
133 sstop = min(start + 5000, stop)
134 lines = nvim.current.buffer.api.get_lines(sstart, sstop, True)
139 for i, line in enumerate(lines):
140 result = function(line, linenr)
142 # Update earlier lines, and skip to the next
144 end = sstart + len(newlines) - 1
145 nvim.current.buffer.api.set_lines(sstart, end,
147 sstart += len(newlines) + 1
150 elif isinstance(result, basestring):
151 newlines.append(result)
153 exception = TypeError('pydo should return a string '
154 + 'or None, found %s instead'
155 % result.__class__.__name__)
161 end = sstart + len(newlines)
162 nvim.current.buffer.api.set_lines(sstart, end, True, newlines)
165 # delete the function
166 del self.module.__dict__[fname]
168 @rpc_export('python_eval', sync=True)
169 def python_eval(self, expr):
170 """Handle the `pyeval` vim function."""
171 return eval(expr, self.module.__dict__)
173 @rpc_export('python_chdir', sync=False)
174 def python_chdir(self, cwd):
175 """Handle working directory changes."""
178 def _set_current_range(self, start, stop):
179 current = self.legacy_vim.current
180 current.range = current.buffer.range(start, stop)
183 class RedirectStream(io.IOBase):
184 def __init__(self, redirect_handler):
185 self.redirect_handler = redirect_handler
187 def write(self, data):
188 self.redirect_handler(data)
190 def writelines(self, seq):
191 self.redirect_handler('\n'.join(seq))
195 num_types = (int, float)
197 num_types = (int, long, float) # noqa: F821
201 if isinstance(obj, num_types):
207 class LegacyVim(Nvim):
208 def eval(self, expr):
209 obj = self.request("vim_eval", expr)
210 return walk(num_to_str, obj)
213 # Copied/adapted from :help if_pyth.
216 if nvim._thread_invalid():
218 return discover_runtime_directories(nvim)
220 def _find_module(fullname, oldtail, path):
221 idx = oldtail.find('.')
224 tail = oldtail[idx + 1:]
225 fmr = imp.find_module(name, path)
226 module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr)
227 return _find_module(fullname, tail, module.__path__)
229 return imp.find_module(fullname, path)
231 class VimModuleLoader(object):
232 def __init__(self, module):
235 def load_module(self, fullname, path=None):
236 # Check sys.modules, required for reload (see PEP302).
238 return sys.modules[fullname]
241 return imp.load_module(fullname, *self.module)
243 class VimPathFinder(object):
245 def find_module(fullname, path=None):
246 """Method for Python 2.7 and 3.3."""
248 return VimModuleLoader(
249 _find_module(fullname, fullname, path or _get_paths()))
254 def find_spec(fullname, target=None):
255 """Method for Python 3.4+."""
256 return PathFinder.find_spec(fullname, _get_paths(), target)
259 if path == nvim.VIM_SPECIAL_PATH:
267 def discover_runtime_directories(nvim):
269 for rtp in nvim.list_runtime_paths():
270 if not os.path.exists(rtp):
272 for subdir in ['pythonx', PYTHON_SUBDIR]:
273 path = os.path.join(rtp, subdir)
274 if os.path.exists(path):