diff options
24 files changed, 1652 insertions, 357 deletions
diff --git a/runtime/autoload/provider/clipboard.vim b/runtime/autoload/provider/clipboard.vim new file mode 100644 index 0000000000..615a80ca6d --- /dev/null +++ b/runtime/autoload/provider/clipboard.vim @@ -0,0 +1,40 @@ +" The clipboard provider uses shell commands to communicate with the clipboard. +" The provider function will only be registered if one of the supported +" commands are available. +let s:copy = '' +let s:paste = '' + +if executable('pbcopy') + let s:copy = 'pbcopy' + let s:paste = 'pbpaste' +elseif executable('xsel') + let s:copy = 'xsel -i -b' + let s:paste = 'xsel -o -b' +elseif executable('xclip') + let s:copy = 'xclip -i -selection clipboard' + let s:paste = 'xclip -o -selection clipboard' +endif + +if s:copy == '' + echom 'No shell command for communicating with the clipboard found.' + finish +endif + +let s:methods = {} + +function! s:ClipboardGet(...) + return systemlist(s:paste) +endfunction + +function! s:ClipboardSet(...) + call systemlist(s:copy, a:1) +endfunction + +let s:methods = { + \ 'get': function('s:ClipboardGet'), + \ 'set': function('s:ClipboardSet') + \ } + +function! provider#clipboard#Call(method, args) + return s:methods[a:method](a:args) +endfunction diff --git a/runtime/autoload/provider/python.vim b/runtime/autoload/provider/python.vim new file mode 100644 index 0000000000..9ca81c35f4 --- /dev/null +++ b/runtime/autoload/provider/python.vim @@ -0,0 +1,28 @@ +" The python provider uses a python host to emulate an environment for running +" python-vim plugins(:h python-vim). See :h nvim-providers for more +" information. +" +" Associating the plugin with the python host is the first step because plugins +" will be passed as command-line arguments +if exists('s:loaded_python_provider') || &cp + finish +endif +let s:loaded_python_provider = 1 +let s:plugin_path = expand('<sfile>:p:h').'/script_host.py' +" The python provider plugin will run in a separate instance of the python +" host. +call rpc#host#RegisterClone('legacy-python-provider', 'python') +call rpc#host#RegisterPlugin('legacy-python-provider', s:plugin_path, []) +" Ensure that we can load the python host before bootstrapping +try + let s:host = rpc#host#Require('legacy-python-provider') +catch + echomsg v:exception + finish +endtry + +let s:rpcrequest = function('rpcrequest') + +function! provider#python#Call(method, args) + return call(s:rpcrequest, insert(insert(a:args, 'python_'.a:method), s:host)) +endfunction diff --git a/runtime/autoload/provider/script_host.py b/runtime/autoload/provider/script_host.py new file mode 100644 index 0000000000..14208310aa --- /dev/null +++ b/runtime/autoload/provider/script_host.py @@ -0,0 +1,243 @@ +"""Legacy python-vim emulation.""" +import imp +import logging +import os +import sys + +import neovim + +__all__ = ('ScriptHost',) + + +logger = logging.getLogger(__name__) +debug, info, warn = (logger.debug, logger.info, logger.warn,) + +IS_PYTHON3 = sys.version_info >= (3, 0) + +if IS_PYTHON3: + basestring = str + + +@neovim.plugin +class ScriptHost(object): + + """Provides an environment for running python plugins created for Vim.""" + + def __init__(self, nvim): + """Initialize the legacy python-vim environment.""" + self.setup(nvim) + # context where all code will run + self.module = imp.new_module('__main__') + nvim.script_context = self.module + # it seems some plugins assume 'sys' is already imported, so do it now + exec('import sys', self.module.__dict__) + self.legacy_vim = nvim.with_hook(LegacyEvalHook()) + if IS_PYTHON3: + self.legacy_vim = self.legacy_vim.with_hook( + neovim.DecodeHook( + encoding=nvim.options['encoding'].decode('ascii'))) + sys.modules['vim'] = self.legacy_vim + + def setup(self, nvim): + """Setup import hooks and global streams. + + This will add import hooks for importing modules from runtime + directories and patch the sys module so 'print' calls will be + forwarded to Nvim. + """ + self.nvim = nvim + info('install import hook/path') + self.hook = path_hook(nvim) + sys.path_hooks.append(self.hook) + nvim.VIM_SPECIAL_PATH = '_vim_path_' + sys.path.append(nvim.VIM_SPECIAL_PATH) + info('redirect sys.stdout and sys.stderr') + self.saved_stdout = sys.stdout + self.saved_stderr = sys.stderr + sys.stdout = RedirectStream(lambda data: nvim.out_write(data)) + sys.stderr = RedirectStream(lambda data: nvim.err_write(data)) + + def teardown(self): + """Restore state modified from the `setup` call.""" + for plugin in self.installed_plugins: + if hasattr(plugin, 'on_teardown'): + plugin.teardown() + nvim = self.nvim + info('uninstall import hook/path') + sys.path.remove(nvim.VIM_SPECIAL_PATH) + sys.path_hooks.remove(self.hook) + info('restore sys.stdout and sys.stderr') + sys.stdout = self.saved_stdout + sys.stderr = self.saved_stderr + + @neovim.rpc_export('python_execute', sync=True) + def python_execute(self, script, range_start, range_stop): + """Handle the `python` ex command.""" + self._set_current_range(range_start, range_stop) + exec(script, self.module.__dict__) + + @neovim.rpc_export('python_execute_file', sync=True) + def python_execute_file(self, file_path, range_start, range_stop): + """Handle the `pyfile` ex command.""" + self._set_current_range(range_start, range_stop) + with open(file_path) as f: + script = compile(f.read(), file_path, 'exec') + exec(script, self.module.__dict__) + + @neovim.rpc_export('python_do_range', sync=True) + def python_do_range(self, start, stop, code): + """Handle the `pydo` ex command.""" + self._set_current_range(start, stop) + nvim = self.nvim + start -= 1 + stop -= 1 + fname = '_vim_pydo' + + # Python3 code (exec) must be a string, mixing bytes with + # function_def would use bytes.__repr__ instead + if isinstance and isinstance(code, bytes): + code = code.decode(nvim.options['encoding'].decode('ascii')) + # define the function + function_def = 'def %s(line, linenr):\n %s' % (fname, code,) + exec(function_def, self.module.__dict__) + # get the function + function = self.module.__dict__[fname] + while start <= stop: + # Process batches of 5000 to avoid the overhead of making multiple + # API calls for every line. Assuming an average line length of 100 + # bytes, approximately 488 kilobytes will be transferred per batch, + # which can be done very quickly in a single API call. + sstart = start + sstop = min(start + 5000, stop) + lines = nvim.current.buffer.get_line_slice(sstart, sstop, True, + True) + + exception = None + newlines = [] + linenr = sstart + 1 + for i, line in enumerate(lines): + result = function(line, linenr) + if result is None: + # Update earlier lines, and skip to the next + if newlines: + end = sstart + len(newlines) - 1 + nvim.current.buffer.set_line_slice(sstart, end, + True, True, + newlines) + sstart += len(newlines) + 1 + newlines = [] + pass + elif isinstance(result, basestring): + newlines.append(result) + else: + exception = TypeError('pydo should return a string ' + + 'or None, found %s instead' + % result.__class__.__name__) + break + linenr += 1 + + start = sstop + 1 + if newlines: + end = sstart + len(newlines) - 1 + nvim.current.buffer.set_line_slice(sstart, end, True, True, + newlines) + if exception: + raise exception + # delete the function + del self.module.__dict__[fname] + + @neovim.rpc_export('python_eval', sync=True) + def python_eval(self, expr): + """Handle the `pyeval` vim function.""" + return eval(expr, self.module.__dict__) + + def _set_current_range(self, start, stop): + current = self.legacy_vim.current + current.range = current.buffer.range(start, stop) + + +class RedirectStream(object): + def __init__(self, redirect_handler): + self.redirect_handler = redirect_handler + + def write(self, data): + self.redirect_handler(data) + + def writelines(self, seq): + self.redirect_handler('\n'.join(seq)) + + +class LegacyEvalHook(neovim.SessionHook): + + """Injects legacy `vim.eval` behavior to a Nvim instance.""" + + def __init__(self): + super(LegacyEvalHook, self).__init__(from_nvim=self._string_eval) + + def _string_eval(self, obj, session, method, kind): + if method == 'vim_eval' and isinstance(obj, (int, long, float)): + return str(obj) + return obj + + +# This was copied/adapted from nvim-python help +def path_hook(nvim): + def _get_paths(): + return discover_runtime_directories(nvim) + + def _find_module(fullname, oldtail, path): + idx = oldtail.find('.') + if idx > 0: + name = oldtail[:idx] + tail = oldtail[idx+1:] + fmr = imp.find_module(name, path) + module = imp.load_module(fullname[:-len(oldtail)] + name, *fmr) + return _find_module(fullname, tail, module.__path__) + else: + fmr = imp.find_module(fullname, path) + return imp.load_module(fullname, *fmr) + + class VimModuleLoader(object): + def __init__(self, module): + self.module = module + + def load_module(self, fullname, path=None): + return self.module + + class VimPathFinder(object): + @classmethod + def find_module(cls, fullname, path=None): + try: + return VimModuleLoader( + _find_module(fullname, fullname, path or _get_paths())) + except ImportError: + return None + + @classmethod + def load_module(cls, fullname, path=None): + return _find_module(fullname, fullname, path or _get_paths()) + + def hook(path): + if path == nvim.VIM_SPECIAL_PATH: + return VimPathFinder + else: + raise ImportError + + return hook + + +def discover_runtime_directories(nvim): + rv = [] + for path in nvim.list_runtime_paths(): + if not os.path.exists(path): + continue + path1 = os.path.join(path, b'pythonx') + if IS_PYTHON3: + path2 = os.path.join(path, b'python3') + else: + path2 = os.path.join(path, b'python2') + if os.path.exists(path1): + rv.append(path1) + if os.path.exists(path2): + rv.append(path2) + return rv diff --git a/runtime/autoload/rpc/define.vim b/runtime/autoload/rpc/define.vim new file mode 100644 index 0000000000..e7817c5fac --- /dev/null +++ b/runtime/autoload/rpc/define.vim @@ -0,0 +1,248 @@ +function! rpc#define#CommandOnHost(host, method, sync, name, opts) + let prefix = '' + + if has_key(a:opts, 'range') + if a:opts.range == '' || a:opts.range == '%' + " -range or -range=%, pass the line range in a list + let prefix = '<line1>,<line2>' + elseif matchstr(a:opts.range, '\d') != '' + " -range=N, pass the count + let prefix = '<count>' + endif + elseif has_key(a:opts, 'count') + let prefix = '<count>' + endif + + let forward_args = [prefix.a:name] + + if has_key(a:opts, 'bang') + call add(forward_args, '<bang>') + endif + + if has_key(a:opts, 'register') + call add(forward_args, ' <register>') + endif + + if has_key(a:opts, 'nargs') + call add(forward_args, ' <args>') + endif + + exe s:GetCommandPrefix(a:name, a:opts) + \ .' call rpc#define#CommandBootstrap("'.a:host.'"' + \ . ', "'.a:method.'"' + \ . ', "'.a:sync.'"' + \ . ', "'.a:name.'"' + \ . ', '.string(a:opts).'' + \ . ', "'.join(forward_args, '').'"' + \ . ')' +endfunction + + +function! rpc#define#CommandBootstrap(host, method, sync, name, opts, forward) + let channel = rpc#host#Require(a:host) + + if channel + call rpc#define#CommandOnChannel(channel, a:method, a:sync, a:name, a:opts) + exe a:forward + else + exe 'delcommand '.a:name + echoerr 'Host "'a:host.'" is not available, deleting command "'.a:name.'"' + endif +endfunction + + +function! rpc#define#CommandOnChannel(channel, method, sync, name, opts) + let rpcargs = [a:channel, '"'.a:method.'"'] + if has_key(a:opts, 'nargs') + " -nargs, pass arguments in a list + call add(rpcargs, '[<f-args>]') + endif + + if has_key(a:opts, 'range') + if a:opts.range == '' || a:opts.range == '%' + " -range or -range=%, pass the line range in a list + call add(rpcargs, '[<line1>, <line2>]') + elseif matchstr(a:opts.range, '\d') != '' + " -range=N, pass the count + call add(rpcargs, '<count>') + endif + elseif has_key(a:opts, 'count') + " count + call add(rpcargs, '<count>') + endif + + if has_key(a:opts, 'bang') + " bang + call add(rpcargs, '<q-bang> == "!"') + endif + + if has_key(a:opts, 'register') + " register + call add(rpcargs, '<q-reg>') + endif + + call s:AddEval(rpcargs, a:opts) + exe s:GetCommandPrefix(a:name, a:opts) + \ . ' call '.s:GetRpcFunction(a:sync).'('.join(rpcargs, ', ').')' +endfunction + + +function! rpc#define#AutocmdOnHost(host, method, sync, name, opts) + let group = s:GetNextAutocmdGroup() + let forward = '"doau '.group.' '.a:name.' ".'.'expand("<amatch>")' + let a:opts.group = group + let bootstrap_def = s:GetAutocmdPrefix(a:name, a:opts) + \ .' call rpc#define#AutocmdBootstrap("'.a:host.'"' + \ . ', "'.a:method.'"' + \ . ', "'.a:sync.'"' + \ . ', "'.a:name.'"' + \ . ', '.string(a:opts).'' + \ . ', "'.escape(forward, '"').'"' + \ . ')' + exe bootstrap_def +endfunction + + +function! rpc#define#AutocmdBootstrap(host, method, sync, name, opts, forward) + let channel = rpc#host#Require(a:host) + + exe 'autocmd! '.a:opts.group + if channel + call rpc#define#AutocmdOnChannel(channel, a:method, a:sync, a:name, + \ a:opts) + exe eval(a:forward) + else + exe 'augroup! '.a:opts.group + echoerr 'Host "'a:host.'" for "'.a:name.'" autocmd is not available' + endif +endfunction + + +function! rpc#define#AutocmdOnChannel(channel, method, sync, name, opts) + let rpcargs = [a:channel, '"'.a:method.'"'] + call s:AddEval(rpcargs, a:opts) + + let autocmd_def = s:GetAutocmdPrefix(a:name, a:opts) + \ . ' call '.s:GetRpcFunction(a:sync).'('.join(rpcargs, ', ').')' + exe autocmd_def +endfunction + + +function! rpc#define#FunctionOnHost(host, method, sync, name, opts) + let group = s:GetNextAutocmdGroup() + exe 'autocmd! '.group.' FuncUndefined '.a:name + \ .' call rpc#define#FunctionBootstrap("'.a:host.'"' + \ . ', "'.a:method.'"' + \ . ', "'.a:sync.'"' + \ . ', "'.a:name.'"' + \ . ', '.string(a:opts) + \ . ', "'.group.'"' + \ . ')' +endfunction + + +function! rpc#define#FunctionBootstrap(host, method, sync, name, opts, group) + let channel = rpc#host#Require(a:host) + + exe 'autocmd! '.a:group + exe 'augroup! '.a:group + if channel + call rpc#define#FunctionOnChannel(channel, a:method, a:sync, a:name, + \ a:opts) + else + echoerr 'Host "'a:host.'" for "'.a:name.'" function is not available' + endif +endfunction + + +function! rpc#define#FunctionOnChannel(channel, method, sync, name, opts) + let rpcargs = [a:channel, '"'.a:method.'"', 'a:000'] + call s:AddEval(rpcargs, a:opts) + + let function_def = s:GetFunctionPrefix(a:name, a:opts) + \ . 'return '.s:GetRpcFunction(a:sync).'('.join(rpcargs, ', ').')' + \ . "\nendfunction" + exe function_def +endfunction + + +function! s:GetRpcFunction(sync) + if a:sync + return 'rpcrequest' + endif + return 'rpcnotify' +endfunction + + +function! s:GetCommandPrefix(name, opts) + return 'command!'.s:StringifyOpts(a:opts, ['nargs', 'complete', 'range', + \ 'count', 'bang', 'bar', 'register']).' '.a:name +endfunction + + +" Each msgpack-rpc autocommand has it's own unique group, which is derived +" from an autoincrementing gid(group id). This is required for replacing the +" autocmd implementation with the lazy-load mechanism +let s:next_gid = 1 +function! s:GetNextAutocmdGroup() + let gid = s:next_gid + let s:next_gid += 1 + + let group_name = 'RPC_DEFINE_AUTOCMD_GROUP_'.gid + " Ensure the group is defined + exe 'augroup '.group_name.' | augroup END' + return group_name +endfunction + + +function! s:GetAutocmdPrefix(name, opts) + if has_key(a:opts, 'group') + let group = a:opts.group + else + let group = s:GetNextAutocmdGroup() + endif + let rv = ['autocmd!', group, a:name] + + if has_key(a:opts, 'pattern') + call add(rv, a:opts.pattern) + else + call add(rv, '*') + endif + + if has_key(a:opts, 'nested') && a:opts.nested + call add(rv, 'nested') + endif + + return join(rv, ' ') +endfunction + + +function! s:GetFunctionPrefix(name, opts) + return "function! ".a:name."(...)\n" +endfunction + + +function! s:StringifyOpts(opts, keys) + let rv = [] + for key in a:keys + if has_key(a:opts, key) + call add(rv, ' -'.key) + let val = a:opts[key] + if type(val) != type('') || val != '' + call add(rv, '='.val) + endif + endif + endfor + return join(rv, '') +endfunction + + +function! s:AddEval(rpcargs, opts) + if has_key(a:opts, 'eval') + if type(a:opts.eval) != type('') || a:opts.eval == '' + throw "Eval option must be a non-empty string" + endif + " evaluate an expression and pass as argument + call add(a:rpcargs, 'eval("'.escape(a:opts.eval, '"').'")') + endif +endfunction diff --git a/runtime/autoload/rpc/host.vim b/runtime/autoload/rpc/host.vim new file mode 100644 index 0000000000..177d816df0 --- /dev/null +++ b/runtime/autoload/rpc/host.vim @@ -0,0 +1,242 @@ +let s:hosts = {} +let s:plugin_patterns = { + \ 'python': '*.py' + \ } +let s:external_plugins = fnamemodify($MYVIMRC, ':p:h').'/.external_plugins~' + + +" Register a host by associating it with a factory(funcref) +function! rpc#host#Register(name, factory) + let s:hosts[a:name] = {'factory': a:factory, 'channel': 0, 'initialized': 0} + if type(a:factory) == type(1) && a:factory + " Passed a channel directly + let s:hosts[a:name].channel = a:factory + endif +endfunction + + +" Register a clone to an existing host. The new host will use the same factory +" as `source`, but it will run as a different process. This can be used by +" plugins that should run isolated from other plugins created for the same host +" type +function! rpc#host#RegisterClone(name, orig_name) + if !has_key(s:hosts, a:orig_name) + throw 'No host named "'.a:orig_name.'" is registered' + endif + let Factory = s:hosts[a:orig_name].factory + let s:hosts[a:name] = {'factory': Factory, 'channel': 0, 'initialized': 0} +endfunction + + +" Get a host channel, bootstrapping it if necessary +function! rpc#host#Require(name) + if !has_key(s:hosts, a:name) + throw 'No host named "'.a:name.'" is registered' + endif + let host = s:hosts[a:name] + if !host.channel && !host.initialized + let host.channel = call(host.factory, [a:name]) + let host.initialized = 1 + endif + return host.channel +endfunction + + +function! rpc#host#IsRunning(name) + if !has_key(s:hosts, a:name) + throw 'No host named "'.a:name.'" is registered' + endif + return s:hosts[a:name].channel != 0 +endfunction + + +" Example of registering a python plugin with two commands(one async), one +" autocmd(async) and one function(sync): +" +" let s:plugin_path = expand('<sfile>:p:h').'/nvim_plugin.py' +" call rpc#host#RegisterPlugin('python', s:plugin_path, [ +" \ {'type': 'command', 'name': 'PyCmd', 'sync': 1, 'opts': {}}, +" \ {'type': 'command', 'name': 'PyAsyncCmd', 'sync': 0, 'opts': {'eval': 'cursor()'}}, +" \ {'type': 'autocmd', 'name': 'BufEnter', 'sync': 0, 'opts': {'eval': 'expand("<afile>")'}}, +" \ {'type': 'function', 'name': 'PyFunc', 'sync': 1, 'opts': {}} +" \ ]) +" +" The third item in a declaration is a boolean: non zero means the command, +" autocommand or function will be executed synchronously with rpcrequest. +function! rpc#host#RegisterPlugin(host, path, specs) + let plugins = s:PluginsForHost(a:host) + + for plugin in plugins + if plugin.path == a:path + throw 'Plugin "'.a:path.'" is already registered' + endif + endfor + + if rpc#host#IsRunning(a:host) + " For now we won't allow registration of plugins when the host is already + " running. + throw 'Host "'.a:host.'" is already running' + endif + + for spec in a:specs + let type = spec.type + let name = spec.name + let sync = spec.sync + let opts = spec.opts + let rpc_method = a:path + if type == 'command' + let rpc_method .= ':command:'.name + call rpc#define#CommandOnHost(a:host, rpc_method, sync, name, opts) + elseif type == 'autocmd' + " Since multiple handlers can be attached to the same autocmd event by a + " single plugin, we need a way to uniquely identify the rpc method to + " call. The solution is to append the autocmd pattern to the method + " name(This still has a limit: one handler per event/pattern combo, but + " there's no need to allow plugins define multiple handlers in that case) + let rpc_method .= ':autocmd:'.name.':'.get(opts, 'pattern', '*') + call rpc#define#AutocmdOnHost(a:host, rpc_method, sync, name, opts) + elseif type == 'function' + let rpc_method .= ':function:'.name + call rpc#define#FunctionOnHost(a:host, rpc_method, sync, name, opts) + else + echoerr 'Invalid declaration type: '.type + endif + endfor + + call add(plugins, {'path': a:path, 'specs': a:specs}) +endfunction + + +function! rpc#host#LoadExternalPlugins() + if filereadable(s:external_plugins) + exe 'source '.s:external_plugins + endif +endfunction + + +function! s:RegistrationCommands(host) + " Register a temporary host clone for discovering specs + let host_id = a:host.'-registration-clone' + call rpc#host#RegisterClone(host_id, a:host) + let pattern = s:plugin_patterns[a:host] + let paths = globpath(&rtp, 'plugin/external/'.a:host.'/'.pattern, 0, 1) + for path in paths + call rpc#host#RegisterPlugin(host_id, path, []) + endfor + let channel = rpc#host#Require(host_id) + let lines = [] + for path in paths + let specs = rpcrequest(channel, 'specs', path) + call add(lines, "call rpc#host#RegisterPlugin('".a:host + \ ."', '".path."', [") + for spec in specs + call add(lines, " \\ ".string(spec).",") + endfor + call add(lines, " \\ ])") + endfor + " Delete the temporary host clone + call rpcstop(s:hosts[host_id].channel) + call remove(s:hosts, host_id) + call remove(s:plugins_for_host, host_id) + return lines +endfunction + + +function! s:UpdateExternalPlugins() + let commands = [] + let hosts = keys(s:hosts) + for host in hosts + if has_key(s:plugin_patterns, host) + let commands = commands + \ + ['" '.host.' plugins'] + \ + s:RegistrationCommands(host) + \ + ['', ''] + endif + endfor + call writefile(commands, s:external_plugins) +endfunction + + +command! UpdateExternalPlugins call s:UpdateExternalPlugins() + + +let s:plugins_for_host = {} +function! s:PluginsForHost(host) + if !has_key(s:plugins_for_host, a:host) + let s:plugins_for_host[a:host] = [] + end + return s:plugins_for_host[a:host] +endfunction + + +" Registration of standard hosts + +" Python {{{ +function! s:RequirePythonHost(name) + " Python host arguments + let args = ['-c', 'import neovim; neovim.start_host()'] + + " Collect registered python plugins into args + let python_plugins = s:PluginsForHost(a:name) + for plugin in python_plugins + call add(args, plugin.path) + endfor + + " Try loading a python host using `python_host_prog` or `python` + let python_host_prog = get(g:, 'python_host_prog', 'python') + try + let channel_id = rpcstart(python_host_prog, args) + if rpcrequest(channel_id, 'poll') == 'ok' + return channel_id + endif + catch + endtry + + " Failed, try a little harder to find the correct interpreter or + " report a friendly error to user + let get_version = + \ ' -c "import sys; sys.stdout.write(str(sys.version_info[0]) + '. + \ '\".\" + str(sys.version_info[1]))"' + + let supported = ['2.6', '2.7'] + + " To load the python host a python executable must be available + if exists('g:python_host_prog') + \ && executable(g:python_host_prog) + \ && index(supported, system(g:python_host_prog.get_version)) >= 0 + let python_host_prog = g:python_host_prog + elseif executable('python') + \ && index(supported, system('python'.get_version)) >= 0 + let python_host_prog = 'python' + elseif executable('python2') + \ && index(supported, system('python2'.get_version)) >= 0 + " In some distros, python3 is the default python + let python_host_prog = 'python2' + else + throw 'No python interpreter found' + endif + + " Make sure we pick correct python version on path. + let python_host_prog = exepath(python_host_prog) + let python_version = systemlist(python_host_prog . ' --version')[0] + + " Execute python, import neovim and print a string. If import_result doesn't + " matches the printed string, the user is missing the neovim module + let import_result = system(python_host_prog . + \ ' -c "import neovim, sys; sys.stdout.write(\"ok\")"') + if import_result != 'ok' + throw 'No neovim module found for ' . python_version + endif + + try + let channel_id = rpcstart(python_host_prog, args) + if rpcrequest(channel_id, 'poll') == 'ok' + return channel_id + endif + catch + endtry + throw 'Failed to load python host' +endfunction + +call rpc#host#Register('python', function('s:RequirePythonHost')) +" }}} diff --git a/runtime/doc/external_plugin.txt b/runtime/doc/external_plugin.txt new file mode 100644 index 0000000000..3f7772c906 --- /dev/null +++ b/runtime/doc/external_plugin.txt @@ -0,0 +1,104 @@ +*external_plugin.txt* For Nvim. {Nvim} + + + NVIM REFERENCE MANUAL by Thiago de Arruda + + +Nvim support for external plugins *external-plugin* + +1. Introduction |external-plugin-intro| +2. Plugin Hosts |external-plugin-hosts| +3. Example |external-plugin-example| + +============================================================================== +1. Introduction *external-plugin-intro* + +A big Nvim goal is to allow extensibility in arbitrary programming languages +without requiring direct support from the editor. This is achieved with +external plugins, coprocesses that have a direct communication channel(via +|msgpack-rpc|) with the Nvim process. + +Even though these plugins are running in separate processes, they can call, be +called, and receive events just as if the code was being executed in the main +process. + +============================================================================== +2. Plugin Hosts *external-plugin-hosts* + +While plugins can be implemented as arbitrary programs that communicate +directly with Nvim API and are called via |rpcrequest()| and |rpcnotify()|, +that is not the best approach available. Instead, developers should first +check if a plugin host implementation is available for their favorite +programming language. + +Plugin hosts are programs that provide a high level environment for plugins, +and also take care of most boilerplate involved in defining commands, autocmds +and functions that are implemented over msgpack-rpc connections. They are +loaded the first time one of it's registered plugins are required, keeping +Nvim startup as fast a possible despite the number of installed plugins/hosts. + +============================================================================== +3. Example *external-plugin-example* + +The best way to learn how to create external plugins is with an example, so +let's see how to implement a very useless python plugin that exports a +command, a function and an autocmd(requires configuration detailed in +|nvim-python| to work). + +The plugin is called 'Limit', and all it does is limit the number of "calls" +made to it. Here's the plugin source code: +> + import neovim + + @neovim.plugin + class Limit(object): + def __init__(self, vim): + self.vim = vim + self.calls = 0 + + @neovim.command('Cmd', range='', nargs='*', sync=True) + def command_handler(self, args, range): + self._increment_calls() + self.vim.current.line = ( + 'Command: Called %d times, args: %s, range: %s' % (self.calls, + args, + range)) + + @neovim.autocmd('BufEnter', pattern='*.py', eval='expand("<afile>")', + sync=True) + def autocmd_handler(self, filename): + self._increment_calls() + self.vim.current.line = ( + 'Autocmd: Called %s times, file: %s' % (self.calls, filename)) + + @neovim.function('Func') + def function_handler(self, args): + self._increment_calls() + self.vim.current.line = ( + 'Function: Called %d times, args: %s' % (self.calls, args)) + + def _increment_calls(self): + if self.calls == 5: + raise Exception('Too many calls!') + self.calls += 1 +< + +This code needs to be saved to "external/python/limit.py" in a runtime +directory(~/.nvim/plugin/external/python/limit.py for example). + +As can be seen, the plugin is implemented using pure python idioms(classes, +methods and decorators). It is the host's responsibility to translate +language-specific idioms to vimscript entities. Notice that the exported +command and autocmd are defined with the "sync" flag, which tells Nvim to call +it using |rpcrequest()|. Since the "Func" doesn't set "sync", it will be +called using |rpcnotify()|. + +Just installing the plugin to ~/.nvim/plugin/external/python/limit.py won't +make Nvim load it at startup. That is because external plugins are loaded +only when required, and for that Nvim must be fed with information about +installed external plugins with the `:UpdateExternalPlugins` command(must be +called whenever plugins are updated, this is analogous to the |:helptags| +command but for external plugins). + +============================================================================== + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/nvim_clipboard.txt b/runtime/doc/nvim_clipboard.txt index 74c9254c70..cf38dea3d6 100644 --- a/runtime/doc/nvim_clipboard.txt +++ b/runtime/doc/nvim_clipboard.txt @@ -6,42 +6,25 @@ Clipboard integration for Nvim *nvim-clipboard* -By default, Nvim has no connection to the system clipboard. Eventually that -will be implemented by UI programs, which connect to Nvim via |msgpack-rpc|. - -Even though externalized UIs are not available yet, there's a workaround that -enables clipboard usage through the python interface(which also uses -|msgpack-rpc| and consequently can implement the clipboard methods required -by Nvim): - -- Make sure you follow the setup instructions in |nvim-python-quickstart|. -- Install the `xerox` python module: - > - $ pip install xerox -< -- Create a ~/.nvim/pythonx/nvim_clipboard.py file with the following contents: - > - import xerox +Nvim has no connection to the system clipboard, instead it is accessible +through the |nvim-provider| infrastructure which transparently uses shell +commands for communicating with the clipboard. - class NvimClipboard(object): - def __init__(self, vim): - self.provides = ['clipboard'] +To use clipboard on Nvim, make sure you have one of the following programs +installed and available on $PATH: - def clipboard_get(self): - return xerox.paste().split('\n') +- xclip +- xsel(newer alternative to xclip) +- pbcopy/pbpaste(already available on Mac OS X) - def clipboard_set(self, lines): - xerox.copy(u'\n'.join([line.decode('utf-8') for line in lines])) -< -This should enable the '+' and '*' registers. As an optional step, set the -'unnamedclip' option to transparently access clipboard using the unnamed -register. If you use the same |vimrc| for both Vim and Nvim, make sure you -only set the option when `has('nvim')` is true: +Having any of these programs should enable the '+' and '*' registers. As an +optional step, set the 'unnamedclip' option to transparently access clipboard +using the unnamed register. If you use the same |vimrc| for both Vim and Nvim, +make sure you only set the option when `has('nvim')` is true: > if has('nvim') set unnamedclip endif < - ============================================================================== vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/nvim_intro.txt b/runtime/doc/nvim_intro.txt index 7b115b1912..177c906db6 100644 --- a/runtime/doc/nvim_intro.txt +++ b/runtime/doc/nvim_intro.txt @@ -17,6 +17,8 @@ differentiate Nvim from Vim: 2. Job control |job-control| 3. Python plugins |nvim-python| 4. Clipboard integration |nvim-clipboard| +5. External plugins |external-plugin| +6. Provider infrastructure |nvim-provider| ============================================================================== vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/nvim_provider.txt b/runtime/doc/nvim_provider.txt new file mode 100644 index 0000000000..e67a5c174c --- /dev/null +++ b/runtime/doc/nvim_provider.txt @@ -0,0 +1,77 @@ +*nvim_provider.txt* For Nvim. {Nvim} + + + NVIM REFERENCE MANUAL by Thiago de Arruda + + +Nvim provider infrastructure *nvim-provider* + +First of all, this document is meant to be read by developers interested in +contributing to the refactoring effort. If you are a normal user or plugin +developer looking to learn about Nvim |msgpack-rpc| infrastructure for +implementing plugins in other programming languages, see |external-plugin|. +For instructions on how to enable python plugins, see |nvim-python|. For +clipboard, see |nvim-clipboard|. + +Instead of doing everything by itself, Nvim aims to simplify it's own +maintenance by delegating as much work as possible to external systems. But +some Vim components are too tightly coupled and in some cases the refactoring +work necessary to swap in-house implementations by code that integrates to +other systems is too great. Nvim provider infrastructure is a facility that +aims to make this task simpler. + +To understand why the provider infrastructure is useful, let us consider two +examples of integration with external systems that are implemented in Vim and +are now decoupled from Nvim core as providers: + +The first example is clipboard integration: On the original Vim source code, +clipboard functions account for more than 1k lines of C source code(and that +is just on ui.c). All to peform two tasks that are now accomplished with +simple shell commands such as xclip or pbcopy/pbpaste. + +The other example is python scripting support: Vim has three files dedicated +to embed the python interpreter: if_python.c, if_python3.c and if_py_both.h. +Together these files sum about 9.5k lines of C source code. On Nvim, python +scripting is performed by an external host process that is running 2k sloc +python program. + +In a perfect world, we would implement python and clipboard integration in +pure vimscript and without touching the C code. Unfortunately we can't achieve +these goals without severly compromising backwards compatibility with Vim. +Thats where providers comes to rescue. + +In essence, this infrastructure a simple framework that simplifies the task of +calling vimscript from C code, making it simpler to rewrite C functions that +interact with external systems in pure vimscript. It is composed of two +functions in eval.c: + +- eval_call_provider(name, method, arguments): Call a provider(name) method + with arguments +- eval_has_provider(name): Checks if a provider is implemented + +What these functions do is simple: + +- eval_call_provider will call the provider#(name)#Call function passing in + the method and arguments. +- eval_has_provider will return true if the provider#(name)#Call function is + implemented, and is called by the "has" vimscript function to check if + features are available. + +The basic idea is that the provider#(name)#Call function should implement +integration with an external system, because calling shell commands and +|msgpack-rpc| clients(Nvim only) is easier to do in vimscript. + +Now, back to the python example. Instead of modifying vimscript to allow the +definition of lowercase functions and commands(for the |:python|, |:pyfile| +and |:pydo| commands, and the |pyeval()| function), which would break +backwards compatibility with Vim, we implemented the +autoload/provider/python.vim script and the provider#python#Call function +that is only defined if an external python host is started successfully. + +That works well with the has('python') expression (normally used by python +plugins) because if the python host isn't installed then the plugin will +"think" it is running in a Vim compiled without +python feature. + + +============================================================================== + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/nvim_python.txt b/runtime/doc/nvim_python.txt index 9e6f408657..e60cc93b19 100644 --- a/runtime/doc/nvim_python.txt +++ b/runtime/doc/nvim_python.txt @@ -27,13 +27,6 @@ simple step-by-step: > $ pip install neovim < -- Add the following snippet to your `vimrc`, before any python plugins are - loaded: -> - if has('nvim') - runtime! python_setup.vim - endif -< Most python plugins created for Vim 7.3 should work after these steps. ============================================================================== diff --git a/runtime/plugin/external_plugins.vim b/runtime/plugin/external_plugins.vim new file mode 100644 index 0000000000..1c5fa1ab8e --- /dev/null +++ b/runtime/plugin/external_plugins.vim @@ -0,0 +1,5 @@ +if exists('loaded_external_plugins') || &cp + finish +endif +let loaded_external_plugins = 1 +call rpc#host#LoadExternalPlugins() diff --git a/runtime/python_setup.vim b/runtime/python_setup.vim deleted file mode 100644 index 0070885958..0000000000 --- a/runtime/python_setup.vim +++ /dev/null @@ -1,60 +0,0 @@ -" Nvim plugin for loading python extensions via an external interpreter -if exists("did_python_setup") || &cp - finish -endif -let did_python_setup = 1 - -" Prefix for naming things and displaying output. -let s:plugin_name = 'Python setup' - -function! s:ShowError(message) - echohl ErrorMsg - echomsg s:plugin_name . ': ' . a:message . '.' - echohl None -endfunction - -let s:get_version = - \ ' -c "import sys; sys.stdout.write(str(sys.version_info[0]) + '. - \ '\".\" + str(sys.version_info[1]))"' - -let s:supported = ['2.6', '2.7'] - -" To load the python host a python 2 executable must be available -if exists('python_interpreter') - \ && executable(g:python_interpreter) - \ && index(s:supported, system(g:python_interpreter.s:get_version)) >= 0 - let s:python_interpreter = g:python_interpreter -elseif executable('python') - \ && index(s:supported, system('python'.s:get_version)) >= 0 - let s:python_interpreter = 'python' -elseif executable('python2') - \ && index(s:supported, system('python2'.s:get_version)) >= 0 - " In some distros, python3 is the default python - let s:python_interpreter = 'python2' -else - call s:ShowError('No python interpreter found') - finish -endif - -" Make sure we pick correct python version on path. -let s:python_interpreter_path = exepath(s:python_interpreter) -let s:python_version = systemlist(s:python_interpreter_path . ' --version')[0] - -" Execute python, import neovim and print a string. If import_result matches -" the printed string, we can probably start the host -let s:import_result = system(s:python_interpreter_path . - \ ' -c "import neovim, sys; sys.stdout.write(\"ok\")"') -if s:import_result != 'ok' - call s:ShowError('No neovim module found for ' . s:python_version) - finish -endif - -let s:pyhost_id = rpcstart(s:python_interpreter_path, - \ ['-c', 'import neovim; neovim.start_host()']) -" Evaluate an expression in the script host as an additional sanity check, and -" to block until all providers have been registered(or else some plugins loaded -" by the user's vimrc would not get has('python') == 1 -if rpcrequest(s:pyhost_id, 'python_eval', '"o"+"k"') != 'ok' || !has('python') - call s:ShowError('Something went wrong setting up ' . s:python_version) - call rpcstop(s:pyhost_id) -endif diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index a7b48f3b7e..750b151d10 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -7,7 +7,6 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/handle.h" -#include "nvim/os/provider.h" #include "nvim/ascii.h" #include "nvim/vim.h" #include "nvim/buffer.h" @@ -548,7 +547,6 @@ Dictionary api_metadata(void) msgpack_rpc_init_function_metadata(&metadata); init_error_type_metadata(&metadata); init_type_metadata(&metadata); - provider_init_feature_metadata(&metadata); } return copy_object(DICTIONARY_OBJ(metadata)).data.dictionary; diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index b6bac1588a..addcbf62e9 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -11,7 +11,6 @@ #include "nvim/api/private/defs.h" #include "nvim/api/buffer.h" #include "nvim/msgpack_rpc/channel.h" -#include "nvim/os/provider.h" #include "nvim/vim.h" #include "nvim/buffer.h" #include "nvim/window.h" @@ -538,22 +537,6 @@ void vim_unsubscribe(uint64_t channel_id, String event) channel_unsubscribe(channel_id, e); } -/// Registers the channel as the provider for `feature`. This fails if -/// a provider for `feature` is already provided by another channel. -/// -/// @param channel_id The channel id -/// @param feature The feature name -/// @param[out] err Details of an error that may have occurred -void vim_register_provider(uint64_t channel_id, String feature, Error *err) -{ - char buf[METHOD_MAXLEN]; - xstrlcpy(buf, feature.data, sizeof(buf)); - - if (!provider_register(buf, channel_id)) { - api_set_error(err, Validation, _("Feature doesn't exist")); - } -} - Array vim_get_api_info(uint64_t channel_id) { Array rv = ARRAY_DICT_INIT; diff --git a/src/nvim/eval.c b/src/nvim/eval.c index a0e6f84259..498795dc38 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -89,7 +89,6 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/vim.h" #include "nvim/os/dl.h" -#include "nvim/os/provider.h" #include "nvim/os/event.h" #define DICT_MAXNEST 100 /* maximum nesting of lists and dicts */ @@ -457,7 +456,7 @@ typedef struct { } JobEvent; #define JobEventFreer(x) KMEMPOOL_INIT(JobEventPool, JobEvent, JobEventFreer) -kmempool_t(JobEventPool) *job_event_pool = NULL; +static kmempool_t(JobEventPool) *job_event_pool = NULL; /* * Initialize the global and v: variables. @@ -5152,7 +5151,7 @@ void list_append_string(list_T *l, char_u *str, int len) /* * Append "n" to list "l". */ -static void list_append_number(list_T *l, varnumber_T n) +void list_append_number(list_T *l, varnumber_T n) { listitem_T *li = listitem_alloc(); li->li_tv.v_type = VAR_NUMBER; @@ -9987,7 +9986,7 @@ static void f_has(typval_T *argvars, typval_T *rettv) } } - if (n == FALSE && provider_has_feature((char *)name)) { + if (n == FALSE && eval_has_provider((char *)name)) { n = TRUE; } @@ -11695,7 +11694,7 @@ static void f_pumvisible(typval_T *argvars, typval_T *rettv) */ static void f_pyeval(typval_T *argvars, typval_T *rettv) { - script_host_eval("python_eval", argvars, rettv); + script_host_eval("python", argvars, rettv); } /* @@ -12540,11 +12539,52 @@ static void f_rpcrequest(typval_T *argvars, typval_T *rettv) ADD(args, vim_to_object(tv)); } + scid_T save_current_SID; + uint8_t *save_sourcing_name, *save_autocmd_fname, *save_autocmd_match; + linenr_T save_sourcing_lnum; + int save_autocmd_fname_full, save_autocmd_bufnr; + void *save_funccalp; + + if (provider_call_nesting) { + // If this is called from a provider function, restore the scope + // information of the caller. + save_current_SID = current_SID; + save_sourcing_name = sourcing_name; + save_sourcing_lnum = sourcing_lnum; + save_autocmd_fname = autocmd_fname; + save_autocmd_match = autocmd_match; + save_autocmd_fname_full = autocmd_fname_full; + save_autocmd_bufnr = autocmd_bufnr; + save_funccalp = save_funccal(); + // + current_SID = provider_caller_scope.SID; + sourcing_name = provider_caller_scope.sourcing_name; + sourcing_lnum = provider_caller_scope.sourcing_lnum; + autocmd_fname = provider_caller_scope.autocmd_fname; + autocmd_match = provider_caller_scope.autocmd_match; + autocmd_fname_full = provider_caller_scope.autocmd_fname_full; + autocmd_bufnr = provider_caller_scope.autocmd_bufnr; + restore_funccal(provider_caller_scope.funccalp); + } + + Error err = ERROR_INIT; Object result = channel_send_call((uint64_t)argvars[0].vval.v_number, (char *)argvars[1].vval.v_string, args, &err); + + if (provider_call_nesting) { + current_SID = save_current_SID; + sourcing_name = save_sourcing_name; + sourcing_lnum = save_sourcing_lnum; + autocmd_fname = save_autocmd_fname; + autocmd_match = save_autocmd_match; + autocmd_fname_full = save_autocmd_fname_full; + autocmd_bufnr = save_autocmd_bufnr; + restore_funccal(save_funccalp); + } + if (err.set) { vim_report_error(cstr_as_string(err.msg)); goto end; @@ -19671,22 +19711,94 @@ static void apply_job_autocmds(int id, char *name, char *type, } } -static void script_host_eval(char *method, typval_T *argvars, typval_T *rettv) +static void script_host_eval(char *name, typval_T *argvars, typval_T *rettv) { - Array args = ARRAY_DICT_INIT; - ADD(args, vim_to_object(argvars)); - Object result = provider_call(method, args); - - if (result.type == kObjectTypeNil) { + if (check_restricted() || check_secure()) { return; } - Error err = ERROR_INIT; - - if (!object_to_vim(result, rettv, &err)){ - EMSG("Error converting value back to vim"); + if (argvars[0].v_type != VAR_STRING) { + EMSG(_(e_invarg)); } - api_free_object(result); + list_T *args = list_alloc(); + list_append_string(args, argvars[0].vval.v_string, -1); + *rettv = eval_call_provider(name, "eval", args); } +typval_T eval_call_provider(char *provider, char *method, list_T *arguments) +{ + char func[256]; + int name_len = snprintf(func, sizeof(func), "provider#%s#Call", provider); + + // Save caller scope information + struct caller_scope saved_provider_caller_scope = provider_caller_scope; + provider_caller_scope = (struct caller_scope) { + .SID = current_SID, + .sourcing_name = sourcing_name, + .sourcing_lnum = sourcing_lnum, + .autocmd_fname = autocmd_fname, + .autocmd_match = autocmd_match, + .autocmd_fname_full = autocmd_fname_full, + .autocmd_bufnr = autocmd_bufnr, + .funccalp = save_funccal() + }; + provider_call_nesting++; + + typval_T argvars[3] = { + {.v_type = VAR_STRING, .vval.v_string = (uint8_t *)method, .v_lock = 0}, + {.v_type = VAR_LIST, .vval.v_list = arguments, .v_lock = 0}, + {.v_type = VAR_UNKNOWN} + }; + typval_T rettv = {.v_type = VAR_UNKNOWN, .v_lock = 0}; + arguments->lv_refcount++; + + int dummy; + (void)call_func((uint8_t *)func, + name_len, + &rettv, + 2, + argvars, + curwin->w_cursor.lnum, + curwin->w_cursor.lnum, + &dummy, + true, + NULL); + + arguments->lv_refcount--; + // Restore caller scope information + restore_funccal(provider_caller_scope.funccalp); + provider_caller_scope = saved_provider_caller_scope; + provider_call_nesting--; + + return rettv; +} + +bool eval_has_provider(char *name) +{ +#define source_provider(name) \ + do_source((uint8_t *)"$VIMRUNTIME/autoload/provider/" name ".vim", \ + false, \ + false) + +#define check_provider(name) \ + if (has_##name == -1) { \ + has_##name = !!find_func((uint8_t *)"provider#" #name "#Call"); \ + if (!has_##name) { \ + source_provider(#name); \ + has_##name = !!find_func((uint8_t *)"provider#" #name "#Call"); \ + } \ + } + + static int has_clipboard = -1, has_python = -1; + + if (!strcmp(name, "clipboard")) { + check_provider(clipboard); + return has_clipboard; + } else if (!strcmp(name, "python")) { + check_provider(python); + return has_python; + } + + return false; +} diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index 1636d62c74..96410897df 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -55,7 +55,6 @@ #include "nvim/os/os.h" #include "nvim/os/shell.h" #include "nvim/os/fs_defs.h" -#include "nvim/os/provider.h" #include "nvim/api/private/helpers.h" #include "nvim/api/private/defs.h" @@ -791,17 +790,17 @@ void ex_profile(exarg_T *eap) void ex_python(exarg_T *eap) { - script_host_execute("python_execute", eap); + script_host_execute("python", eap); } void ex_pyfile(exarg_T *eap) { - script_host_execute_file("python_execute_file", eap); + script_host_execute_file("python", eap); } void ex_pydo(exarg_T *eap) { - script_host_do_range("python_do_range", eap); + script_host_do_range("python", eap); } @@ -3254,46 +3253,43 @@ char_u *get_locales(expand_T *xp, int idx) #endif -static void script_host_execute(char *method, exarg_T *eap) +static void script_host_execute(char *name, exarg_T *eap) { - char *script = (char *)script_get(eap, eap->arg); + uint8_t *script = script_get(eap, eap->arg); if (!eap->skip) { - Array args = ARRAY_DICT_INIT; - ADD(args, STRING_OBJ(cstr_to_string(script ? script : (char *)eap->arg))); - // add current range - ADD(args, INTEGER_OBJ(eap->line1)); - ADD(args, INTEGER_OBJ(eap->line2)); - Object result = provider_call(method, args); - // We don't care about the result, so free it just in case a bad provider - // returned something - api_free_object(result); + list_T *args = list_alloc(); + // script + list_append_string(args, script ? script : eap->arg, -1); + // current range + list_append_number(args, eap->line1); + list_append_number(args, eap->line2); + (void)eval_call_provider(name, "execute", args); } free(script); } -static void script_host_execute_file(char *method, exarg_T *eap) +static void script_host_execute_file(char *name, exarg_T *eap) { - char buffer[MAXPATHL]; - vim_FullName(eap->arg, (uint8_t *)buffer, sizeof(buffer), false); + uint8_t buffer[MAXPATHL]; + vim_FullName(eap->arg, buffer, sizeof(buffer), false); - Array args = ARRAY_DICT_INIT; - ADD(args, STRING_OBJ(cstr_to_string(buffer))); - // add current range - ADD(args, INTEGER_OBJ(eap->line1)); - ADD(args, INTEGER_OBJ(eap->line2)); - Object result = provider_call(method, args); - api_free_object(result); + list_T *args = list_alloc(); + // filename + list_append_string(args, buffer, -1); + // current range + list_append_number(args, eap->line1); + list_append_number(args, eap->line2); + (void)eval_call_provider(name, "execute_file", args); } -static void script_host_do_range(char *method, exarg_T *eap) +static void script_host_do_range(char *name, exarg_T *eap) { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(eap->line1)); - ADD(args, INTEGER_OBJ(eap->line2)); - ADD(args, STRING_OBJ(cstr_to_string((char *)eap->arg))); - Object result = provider_call(method, args); - api_free_object(result); + list_T *args = list_alloc(); + list_append_number(args, eap->line1); + list_append_number(args, eap->line2); + list_append_string(args, eap->arg, -1); + (void)eval_call_provider(name, "do_range", args); } diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 2a4d6da8dc..cd9f7a648f 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -360,6 +360,16 @@ EXTERN int garbage_collect_at_exit INIT(= FALSE); /* ID of script being sourced or was sourced to define the current function. */ EXTERN scid_T current_SID INIT(= 0); +// Scope information for the code that indirectly triggered the current +// provider function call +EXTERN struct caller_scope { + scid_T SID; + uint8_t *sourcing_name, *autocmd_fname, *autocmd_match; + linenr_T sourcing_lnum; + int autocmd_fname_full, autocmd_bufnr; + void *funccalp; +} provider_caller_scope; +EXTERN int provider_call_nesting INIT(= 0); /* Magic number used for hashitem "hi_key" value indicating a deleted item. * Only the address is used. */ diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index da1dc8d1b5..0c04a7b23e 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -137,7 +137,6 @@ uint64_t channel_from_job(char **argv) &status); if (status <= 0) { - free_channel(channel); return 0; } @@ -381,7 +380,7 @@ static void parse_msgpack(RStream *rstream, void *data, bool eof) bool is_response = is_rpc_response(&unpacked.data); log_client_msg(channel->id, !is_response, unpacked.data); - if (kv_size(channel->call_stack) && is_response) { + if (is_response) { if (is_valid_rpc_response(&unpacked.data, channel)) { complete_call(&unpacked.data, channel); } else { @@ -389,8 +388,8 @@ static void parse_msgpack(RStream *rstream, void *data, bool eof) snprintf(buf, sizeof(buf), "Channel %" PRIu64 " returned a response that doesn't have " - " a matching id for the current RPC call. Ensure the client " - " is properly synchronized", + "a matching request id. Ensure the client is properly " + "synchronized", channel->id); call_set_error(channel, buf); } @@ -685,8 +684,8 @@ static bool is_valid_rpc_response(msgpack_object *obj, Channel *channel) { uint64_t response_id = obj->via.array.ptr[1].via.u64; // Must be equal to the frame at the stack's bottom - return response_id == kv_A(channel->call_stack, - kv_size(channel->call_stack) - 1)->request_id; + return kv_size(channel->call_stack) && response_id + == kv_A(channel->call_stack, kv_size(channel->call_stack) - 1)->request_id; } static void complete_call(msgpack_object *obj, Channel *channel) diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 2523e21c42..5ef605bb3b 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -48,7 +48,6 @@ #include "nvim/ui.h" #include "nvim/undo.h" #include "nvim/window.h" -#include "nvim/os/provider.h" #include "nvim/api/private/helpers.h" /* @@ -1308,7 +1307,7 @@ bool adjust_clipboard_register(int *rp) { // If no reg. specified and 'unnamedclip' is set, use the // clipboard register. - if (*rp == 0 && p_unc && provider_has_feature("clipboard")) { + if (*rp == 0 && p_unc && eval_has_provider("clipboard")) { *rp = '+'; return true; } @@ -5240,28 +5239,29 @@ static void copy_register(struct yankreg *dest, struct yankreg *src) static void get_clipboard(int name) { if (!(name == '*' || name == '+' - || (p_unc && !name && provider_has_feature("clipboard")))) { + || (p_unc && !name && eval_has_provider("clipboard")))) { return; } struct yankreg *reg = &y_regs[CLIP_REGISTER]; free_register(reg); - Array args = ARRAY_DICT_INIT; - Object result = provider_call("clipboard_get", args); + list_T *args = list_alloc(); + typval_T result = eval_call_provider("clipboard", "get", args); - if (result.type != kObjectTypeArray) { + if (result.v_type != VAR_LIST) { goto err; } - Array lines = result.data.array; - reg->y_array = xcalloc(lines.size, sizeof(uint8_t *)); - reg->y_size = lines.size; + list_T *lines = result.vval.v_list; + reg->y_array = xcalloc(lines->lv_len, sizeof(uint8_t *)); + reg->y_size = lines->lv_len; - for (size_t i = 0; i < lines.size; i++) { - if (lines.items[i].type != kObjectTypeString) { + int i = 0; + for (listitem_T *li = lines->lv_first; li != NULL; li = li->li_next) { + if (li->li_tv.v_type != VAR_STRING) { goto err; } - reg->y_array[i] = (uint8_t *)lines.items[i].data.string.data; + reg->y_array[i++] = (uint8_t *)xstrdup((char *)li->li_tv.vval.v_string); } if (!name && p_unc) { @@ -5272,8 +5272,12 @@ static void get_clipboard(int name) return; err: - api_free_object(result); - free(reg->y_array); + if (reg->y_array) { + for (int i = 0; i < reg->y_size; i++) { + free(reg->y_array[i]); + } + free(reg->y_array); + } reg->y_array = NULL; reg->y_size = 0; EMSG("Clipboard provider returned invalid data"); @@ -5282,7 +5286,7 @@ err: static void set_clipboard(int name) { if (!(name == '*' || name == '+' - || (p_unc && !name && provider_has_feature("clipboard")))) { + || (p_unc && !name && eval_has_provider("clipboard")))) { return; } @@ -5293,15 +5297,11 @@ static void set_clipboard(int name) copy_register(reg, &y_regs[0]); } - Array lines = ARRAY_DICT_INIT; + list_T *lines = list_alloc(); for (int i = 0; i < reg->y_size; i++) { - ADD(lines, STRING_OBJ(cstr_to_string((char *)reg->y_array[i]))); + list_append_string(lines, reg->y_array[i], -1); } - Array args = ARRAY_DICT_INIT; - ADD(args, ARRAY_OBJ(lines)); - - Object result = provider_call("clipboard_set", args); - api_free_object(result); + (void)eval_call_provider("clipboard", "set", lines); } diff --git a/src/nvim/os/event.c b/src/nvim/os/event.c index 5a5da5cd63..1477072f4f 100644 --- a/src/nvim/os/event.c +++ b/src/nvim/os/event.c @@ -11,7 +11,6 @@ #include "nvim/msgpack_rpc/channel.h" #include "nvim/msgpack_rpc/server.h" #include "nvim/msgpack_rpc/helpers.h" -#include "nvim/os/provider.h" #include "nvim/os/signal.h" #include "nvim/os/rstream.h" #include "nvim/os/wstream.h" @@ -62,8 +61,6 @@ void event_init(void) // finish mspgack-rpc initialization channel_init(); server_init(); - // Providers - provider_init(); } void event_teardown(void) diff --git a/src/nvim/os/provider.c b/src/nvim/os/provider.c deleted file mode 100644 index 414c8841fa..0000000000 --- a/src/nvim/os/provider.c +++ /dev/null @@ -1,152 +0,0 @@ -#include <stdint.h> -#include <inttypes.h> -#include <stdbool.h> -#include <assert.h> - -#include "nvim/os/provider.h" -#include "nvim/memory.h" -#include "nvim/api/vim.h" -#include "nvim/api/private/helpers.h" -#include "nvim/api/private/defs.h" -#include "nvim/msgpack_rpc/channel.h" -#include "nvim/os/shell.h" -#include "nvim/os/os.h" -#include "nvim/log.h" -#include "nvim/map.h" -#include "nvim/message.h" - -#define FEATURE_COUNT (sizeof(features) / sizeof(features[0])) - -#define FEATURE(feature_name, ...) { \ - .name = feature_name, \ - .channel_id = 0, \ - .methods = (char *[]){__VA_ARGS__, NULL} \ -} - -typedef struct { - char *name, **methods; - size_t name_length; - uint64_t channel_id; -} Feature; - -static Feature features[] = { - FEATURE("python", - "python_execute", - "python_execute_file", - "python_do_range", - "python_eval"), - - FEATURE("clipboard", - "clipboard_get", - "clipboard_set") -}; - -static PMap(cstr_t) *registered_providers = NULL; - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "os/provider.c.generated.h" -#endif - - -void provider_init(void) -{ - registered_providers = pmap_new(cstr_t)(); -} - -bool provider_has_feature(char *name) -{ - Feature *f = find_feature(name); - return f != NULL && channel_exists(f->channel_id); -} - -bool provider_register(char *name, uint64_t channel_id) -{ - Feature *f = find_feature(name); - - if (!f) { - return false; - } - - if (f->channel_id && channel_exists(f->channel_id)) { - ILOG("Feature \"%s\" is already provided by another channel" - "(will be replaced)", name); - } - - DLOG("Registering provider for \"%s\"", name); - f->channel_id = channel_id; - - // Associate all method names with the feature struct - size_t i; - char *method; - for (method = f->methods[i = 0]; method; method = f->methods[++i]) { - pmap_put(cstr_t)(registered_providers, method, f); - DLOG("Channel \"%" PRIu64 "\" will be sent requests for \"%s\"", - channel_id, - method); - } - - ILOG("Registered channel %" PRIu64 " as the provider for the \"%s\" feature", - channel_id, - name); - - return true; -} - -Object provider_call(char *method, Array args) -{ - Feature *f = pmap_get(cstr_t)(registered_providers, method); - - if (!f || !channel_exists(f->channel_id)) { - char buf[256]; - snprintf(buf, - sizeof(buf), - "Provider for method \"%s\" is not available", - method); - vim_report_error(cstr_as_string(buf)); - api_free_array(args); - return NIL; - } - - Error err = ERROR_INIT; - Object result = NIL = channel_send_call(f->channel_id, method, args, &err); - - if (err.set) { - vim_report_error(cstr_as_string(err.msg)); - api_free_object(result); - return NIL; - } - - return result; -} - -void provider_init_feature_metadata(Dictionary *metadata) -{ - Dictionary md = ARRAY_DICT_INIT; - - for (size_t i = 0; i < FEATURE_COUNT; i++) { - Array methods = ARRAY_DICT_INIT; - Feature *f = &features[i]; - - size_t j; - char *method; - for (method = f->methods[j = 0]; method; method = f->methods[++j]) { - ADD(methods, STRING_OBJ(cstr_to_string(method))); - } - - PUT(md, f->name, ARRAY_OBJ(methods)); - } - - PUT(*metadata, "features", DICTIONARY_OBJ(md)); -} - -static Feature * find_feature(char *name) -{ - for (size_t i = 0; i < FEATURE_COUNT; i++) { - Feature *f = &features[i]; - if (!STRICMP(name, f->name)) { - return f; - } - } - - return NULL; -} diff --git a/src/nvim/os/provider.h b/src/nvim/os/provider.h deleted file mode 100644 index c6f12e02dd..0000000000 --- a/src/nvim/os/provider.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef NVIM_OS_PROVIDER_H -#define NVIM_OS_PROVIDER_H - -#include "nvim/api/private/defs.h" - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "os/provider.h.generated.h" -#endif - -#endif // NVIM_OS_PROVIDER_H - diff --git a/test/functional/runtime/autoload/provider/python_spec.lua b/test/functional/runtime/autoload/provider/python_spec.lua new file mode 100644 index 0000000000..1a726652d6 --- /dev/null +++ b/test/functional/runtime/autoload/provider/python_spec.lua @@ -0,0 +1,93 @@ +do + local proc = + io.popen([[python -c 'import neovim, sys; sys.stdout.write("ok")' 2> /dev/null]]) + if proc:read() ~= 'ok' then + -- Don't run these tests if python is not available + return + end +end + + +local helpers = require('test.functional.helpers') +local eval, command, feed = helpers.eval, helpers.command, helpers.feed +local eq, clear, insert = helpers.eq, helpers.clear, helpers.insert +local expect = helpers.expect + + +describe('python commands and functions', function() + before_each(function() + clear() + command('python import vim') + end) + + describe('feature test', function() + it('ok', function() + eq(1, eval('has("python")')) + end) + end) + + describe('python_execute', function() + it('ok', function() + command('python vim.vars["set_by_python"] = [100, 0]') + eq({100, 0}, eval('g:set_by_python')) + end) + end) + + describe('python_execute with nested commands', function() + it('ok', function() + command([[python vim.command('python vim.command("python vim.command(\'let set_by_nested_python = 555\')")')]]) + eq(555, eval('g:set_by_nested_python')) + end) + end) + + describe('python_execute with range', function() + it('ok', function() + insert([[ + line1 + line2 + line3 + line4]]) + feed('ggjvj:python vim.vars["range"] = vim.current.range[:]<CR>') + eq({'line2', 'line3'}, eval('g:range')) + end) + end) + + describe('pyfile', function() + it('ok', function() + local fname = 'pyfile.py' + local F = io.open(fname, 'w') + F:write('vim.command("let set_by_pyfile = 123")') + F:close() + command('pyfile pyfile.py') + eq(123, eval('g:set_by_pyfile')) + os.remove(fname) + end) + end) + + describe('pydo', function() + it('ok', function() + -- :pydo 42 returns None for all lines, + -- the buffer should not be changed + command('normal :pydo 42') + eq(0, eval('&mod')) + -- insert some text + insert('abc\ndef\nghi') + expect([[ + abc + def + ghi]]) + -- go to top and select and replace the first two lines + feed('ggvj:pydo return str(linenr)<CR>') + expect([[ + 1 + 2 + ghi]]) + end) + end) + + describe('pyeval', function() + it('ok', function() + eq({1, 2, {['key'] = 'val'}}, eval([[pyeval('[1, 2, {"key": "val"}]')]])) + end) + end) +end) diff --git a/test/functional/runtime/autoload/rpc/define_spec.lua b/test/functional/runtime/autoload/rpc/define_spec.lua new file mode 100644 index 0000000000..a85ba4d37b --- /dev/null +++ b/test/functional/runtime/autoload/rpc/define_spec.lua @@ -0,0 +1,365 @@ +local helpers = require('test.functional.helpers') +local eval, command, nvim = helpers.eval, helpers.command, helpers.nvim +local eq, run, stop = helpers.eq, helpers.run, helpers.stop +local clear, feed = helpers.clear, helpers.feed + + +local function get_prefix(sync) + if sync then + return 'sync' + end + return 'async' +end + + +local function call(fn, args) + command('call '..fn..'('..args..')') +end + + +local function clear_and_init(init) + return function() + clear() + if init then + init() + end + end +end + + +local function runx(sync, handler, on_setup) + local function setup_cb(...) + on_setup(...) + -- need to stop on setup callback because there's two session:request + -- calls in `request/helpers.lua`. The second call will always return + -- after pending notification/request callbacks are processed + stop() + end + local function handler_cb(...) + return handler(...) + end + if sync then + run(handler_cb, nil, setup_cb) + else + run(nil, handler_cb, setup_cb) + end +end + +local function command_specs_for(fn, sync, first_arg_factory, init) + local prefix = get_prefix(sync) + + describe(prefix..' command created by', function() + before_each(clear_and_init(init)) + + describe(fn, function() + local args + + before_each(function() + args = first_arg_factory()..', "test-handler", ' + if sync then + args = args .. '1' + else + args = args .. '0' + end + args = args..', "RpcCommand"' + end) + + describe('without options', function() + it('ok', function() + call(fn, args..', {}') + local function on_setup() + command('RpcCommand') + end + + local function handler(method) + eq('test-handler', method) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + + describe('with nargs', function() + it('ok', function() + call(fn, args..', {"nargs": "*"}') + local function on_setup() + command('RpcCommand arg1 arg2 arg3') + end + + local function handler(method, args) + eq('test-handler', method) + eq({'arg1', 'arg2', 'arg3'}, args[1]) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + + describe('with range', function() + it('ok', function() + call(fn,args..', {"range": ""}') + local function on_setup() + command('1,1RpcCommand') + end + + local function handler(method, args) + eq('test-handler', method) + eq({1, 1}, args[1]) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + + describe('with nargs/range', function() + it('ok', function() + call(fn, args..', {"nargs": "1", "range": ""}') + local function on_setup() + command('1,1RpcCommand arg') + end + + local function handler(method, args) + eq('test-handler', method) + eq({'arg'}, args[1]) + eq({1, 1}, args[2]) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + + describe('with nargs/count', function() + it('ok', function() + call(fn, args..', {"nargs": "1", "range": "5"}') + local function on_setup() + command('5RpcCommand arg') + end + + local function handler(method, args) + eq('test-handler', method) + eq({'arg'}, args[1]) + eq(5, args[2]) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + + describe('with nargs/count/bang', function() + it('ok', function() + call(fn, args..', {"nargs": "1", "range": "5", "bang": ""}') + local function on_setup() + command('5RpcCommand! arg') + end + + local function handler(method, args) + eq('test-handler', method) + eq({'arg'}, args[1]) + eq(5, args[2]) + eq(1, args[3]) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + + describe('with nargs/count/bang/register', function() + it('ok', function() + call(fn, args..', {"nargs": "1", "range": "5", "bang": "",'.. + ' "register": ""}') + local function on_setup() + command('5RpcCommand! b arg') + end + + local function handler(method, args) + eq('test-handler', method) + eq({'arg'}, args[1]) + eq(5, args[2]) + eq(1, args[3]) + eq('b', args[4]) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + + describe('with nargs/count/bang/register/eval', function() + it('ok', function() + call(fn, args..', {"nargs": "1", "range": "5", "bang": "",'.. + ' "register": "", "eval": "@<reg>"}') + local function on_setup() + command('let @b = "regb"') + command('5RpcCommand! b arg') + end + + local function handler(method, args) + eq('test-handler', method) + eq({'arg'}, args[1]) + eq(5, args[2]) + eq(1, args[3]) + eq('b', args[4]) + eq('regb', args[5]) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + end) + end) +end + + +local function autocmd_specs_for(fn, sync, first_arg_factory, init) + local prefix = get_prefix(sync) + + describe(prefix..' autocmd created by', function() + before_each(clear_and_init(init)) + + describe(fn, function() + local args + + before_each(function() + args = first_arg_factory()..', "test-handler", ' + if sync then + args = args .. '1' + else + args = args .. '0' + end + args = args..', "BufEnter"' + end) + + describe('without options', function() + it('ok', function() + call(fn, args..', {}') + local function on_setup() + command('doautocmd BufEnter x.c') + end + + local function handler(method, args) + eq('test-handler', method) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + + describe('with eval', function() + it('ok', function() + call(fn, args..[[, {'eval': 'expand("<afile>")'}]]) + local function on_setup() + command('doautocmd BufEnter x.c') + end + + local function handler(method, args) + eq('test-handler', method) + eq('x.c', args[1]) + return '' + end + + runx(sync, handler, on_setup) + end) + end) + end) + end) +end + + +local function function_specs_for(fn, sync, first_arg_factory, init) + local prefix = get_prefix(sync) + + describe(prefix..' function created by', function() + before_each(clear_and_init(init)) + + describe(fn, function() + local args + + before_each(function() + args = first_arg_factory()..', "test-handler", ' + if sync then + args = args .. '1' + else + args = args .. '0' + end + args = args..', "TestFunction"' + end) + + describe('without options', function() + it('ok', function() + call(fn, args..', {}') + local function on_setup() + if sync then + eq('rv', eval('TestFunction(1, "a", ["b", "c"])')) + else + eq(1, eval('TestFunction(1, "a", ["b", "c"])')) + end + end + + local function handler(method, args) + eq('test-handler', method) + eq({{1, 'a', {'b', 'c'}}}, args) + return 'rv' + end + + runx(sync, handler, on_setup) + end) + end) + + describe('with eval', function() + it('ok', function() + call(fn, args..[[, {'eval': '2 + 2'}]]) + local function on_setup() + if sync then + eq('rv', eval('TestFunction(1, "a", ["b", "c"])')) + else + eq(1, eval('TestFunction(1, "a", ["b", "c"])')) + end + end + + local function handler(method, args) + eq('test-handler', method) + eq({{1, 'a', {'b', 'c'}}, 4}, args) + return 'rv' + end + + runx(sync, handler, on_setup) + end) + end) + end) + end) +end + +local function channel() + return nvim('get_api_info')[1] +end + +local function host() + return '"busted"' +end + +local function register() + eval('rpc#host#Register("busted", '..channel()..')') +end + +command_specs_for('rpc#define#CommandOnChannel', true, channel) +command_specs_for('rpc#define#CommandOnChannel', false, channel) +command_specs_for('rpc#define#CommandOnHost', true, host, register) +command_specs_for('rpc#define#CommandOnHost', false, host, register) + +autocmd_specs_for('rpc#define#AutocmdOnChannel', true, channel) +autocmd_specs_for('rpc#define#AutocmdOnChannel', false, channel) +autocmd_specs_for('rpc#define#AutocmdOnHost', true, host, register) +autocmd_specs_for('rpc#define#AutocmdOnHost', false, host, register) + +function_specs_for('rpc#define#FunctionOnChannel', true, channel) +function_specs_for('rpc#define#FunctionOnChannel', false, channel) +function_specs_for('rpc#define#FunctionOnHost', true, host, register) +function_specs_for('rpc#define#FunctionOnHost', false, host, register) |