aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/autoload/provider/clipboard.vim40
-rw-r--r--runtime/autoload/provider/python.vim28
-rw-r--r--runtime/autoload/provider/script_host.py243
-rw-r--r--runtime/autoload/rpc/define.vim248
-rw-r--r--runtime/autoload/rpc/host.vim242
-rw-r--r--runtime/doc/external_plugin.txt104
-rw-r--r--runtime/doc/nvim_clipboard.txt41
-rw-r--r--runtime/doc/nvim_intro.txt2
-rw-r--r--runtime/doc/nvim_provider.txt77
-rw-r--r--runtime/doc/nvim_python.txt7
-rw-r--r--runtime/plugin/external_plugins.vim5
-rw-r--r--runtime/python_setup.vim60
-rw-r--r--src/nvim/api/private/helpers.c2
-rw-r--r--src/nvim/api/vim.c17
-rw-r--r--src/nvim/eval.c144
-rw-r--r--src/nvim/ex_cmds2.c60
-rw-r--r--src/nvim/globals.h10
-rw-r--r--src/nvim/msgpack_rpc/channel.c11
-rw-r--r--src/nvim/ops.c44
-rw-r--r--src/nvim/os/event.c3
-rw-r--r--src/nvim/os/provider.c152
-rw-r--r--src/nvim/os/provider.h11
-rw-r--r--test/functional/runtime/autoload/provider/python_spec.lua93
-rw-r--r--test/functional/runtime/autoload/rpc/define_spec.lua365
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)