efficient vim config
[dotfiles/.git] / .local / lib / python2.7 / site-packages / pynvim / plugin / script_host.py
1 """Legacy python/python3-vim emulation."""
2 import imp
3 import io
4 import logging
5 import os
6 import sys
7 from types import ModuleType
8
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
14
15 __all__ = ('ScriptHost',)
16
17
18 logger = logging.getLogger(__name__)
19 debug, info, warn = (logger.debug, logger.info, logger.warn,)
20
21 if IS_PYTHON3:
22     basestring = str
23
24     if sys.version_info >= (3, 4):
25         from importlib.machinery import PathFinder
26
27     PYTHON_SUBDIR = 'python3'
28 else:
29     PYTHON_SUBDIR = 'python2'
30
31
32 @plugin
33 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 = 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
50         nvim.command(
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)
60
61     def setup(self, nvim):
62         """Setup import hooks and global streams.
63
64         This will add import hooks for importing modules from runtime
65         directories and patch the sys module so 'print' calls will be
66         forwarded to Nvim.
67         """
68         self.nvim = nvim
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))
81
82     def teardown(self):
83         """Restore state modified from the `setup` call."""
84         nvim = self.nvim
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
93
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)
98         try:
99             exec(script, self.module.__dict__)
100         except Exception:
101             raise ErrorResponse(format_exc_skip(1))
102
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')
109             try:
110                 exec(script, self.module.__dict__)
111             except Exception:
112                 raise ErrorResponse(format_exc_skip(1))
113
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)
118         nvim = self.nvim
119         start -= 1
120         fname = '_vim_pydo'
121
122         # define the function
123         function_def = 'def %s(line, linenr):\n %s' % (fname, code,)
124         exec(function_def, self.module.__dict__)
125         # get the function
126         function = self.module.__dict__[fname]
127         while start < stop:
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.
132             sstart = start
133             sstop = min(start + 5000, stop)
134             lines = nvim.current.buffer.api.get_lines(sstart, sstop, True)
135
136             exception = None
137             newlines = []
138             linenr = sstart + 1
139             for i, line in enumerate(lines):
140                 result = function(line, linenr)
141                 if result is None:
142                     # Update earlier lines, and skip to the next
143                     if newlines:
144                         end = sstart + len(newlines) - 1
145                         nvim.current.buffer.api.set_lines(sstart, end,
146                                                           True, newlines)
147                     sstart += len(newlines) + 1
148                     newlines = []
149                     pass
150                 elif isinstance(result, basestring):
151                     newlines.append(result)
152                 else:
153                     exception = TypeError('pydo should return a string '
154                                           + 'or None, found %s instead'
155                                           % result.__class__.__name__)
156                     break
157                 linenr += 1
158
159             start = sstop
160             if newlines:
161                 end = sstart + len(newlines)
162                 nvim.current.buffer.api.set_lines(sstart, end, True, newlines)
163             if exception:
164                 raise exception
165         # delete the function
166         del self.module.__dict__[fname]
167
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__)
172
173     @rpc_export('python_chdir', sync=False)
174     def python_chdir(self, cwd):
175         """Handle working directory changes."""
176         os.chdir(cwd)
177
178     def _set_current_range(self, start, stop):
179         current = self.legacy_vim.current
180         current.range = current.buffer.range(start, stop)
181
182
183 class RedirectStream(io.IOBase):
184     def __init__(self, redirect_handler):
185         self.redirect_handler = redirect_handler
186
187     def write(self, data):
188         self.redirect_handler(data)
189
190     def writelines(self, seq):
191         self.redirect_handler('\n'.join(seq))
192
193
194 if IS_PYTHON3:
195     num_types = (int, float)
196 else:
197     num_types = (int, long, float)  # noqa: F821
198
199
200 def num_to_str(obj):
201     if isinstance(obj, num_types):
202         return str(obj)
203     else:
204         return obj
205
206
207 class LegacyVim(Nvim):
208     def eval(self, expr):
209         obj = self.request("vim_eval", expr)
210         return walk(num_to_str, obj)
211
212
213 # Copied/adapted from :help if_pyth.
214 def path_hook(nvim):
215     def _get_paths():
216         if nvim._thread_invalid():
217             return []
218         return discover_runtime_directories(nvim)
219
220     def _find_module(fullname, oldtail, path):
221         idx = oldtail.find('.')
222         if idx > 0:
223             name = oldtail[:idx]
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__)
228         else:
229             return imp.find_module(fullname, path)
230
231     class VimModuleLoader(object):
232         def __init__(self, module):
233             self.module = module
234
235         def load_module(self, fullname, path=None):
236             # Check sys.modules, required for reload (see PEP302).
237             try:
238                 return sys.modules[fullname]
239             except KeyError:
240                 pass
241             return imp.load_module(fullname, *self.module)
242
243     class VimPathFinder(object):
244         @staticmethod
245         def find_module(fullname, path=None):
246             """Method for Python 2.7 and 3.3."""
247             try:
248                 return VimModuleLoader(
249                     _find_module(fullname, fullname, path or _get_paths()))
250             except ImportError:
251                 return None
252
253         @staticmethod
254         def find_spec(fullname, target=None):
255             """Method for Python 3.4+."""
256             return PathFinder.find_spec(fullname, _get_paths(), target)
257
258     def hook(path):
259         if path == nvim.VIM_SPECIAL_PATH:
260             return VimPathFinder
261         else:
262             raise ImportError
263
264     return hook
265
266
267 def discover_runtime_directories(nvim):
268     rv = []
269     for rtp in nvim.list_runtime_paths():
270         if not os.path.exists(rtp):
271             continue
272         for subdir in ['pythonx', PYTHON_SUBDIR]:
273             path = os.path.join(rtp, subdir)
274             if os.path.exists(path):
275                 rv.append(path)
276     return rv