From 6b17082d3cf1f2e6c9eddb4f5ec0154f0f7286b0 Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Fri, 21 Nov 2014 10:07:59 -0300 Subject: runtime: Refer to plugins running outside Nvim as "remote plugins" - Rename autoload/rpc to autoload/remote - External plugins are now remote plugins - External plugins directory is "rplugin" --- runtime/autoload/provider/python.vim | 6 +- runtime/autoload/remote/define.vim | 248 ++++++++++++++ runtime/autoload/remote/host.vim | 243 ++++++++++++++ runtime/autoload/rpc/define.vim | 248 -------------- runtime/autoload/rpc/host.vim | 243 -------------- runtime/doc/Makefile | 4 +- runtime/doc/external_plugin.txt | 136 -------- runtime/doc/nvim_intro.txt | 2 +- runtime/doc/remote_plugin.txt | 136 ++++++++ runtime/plugin/external_plugins.vim | 5 - runtime/plugin/rplugin.vim | 5 + .../runtime/autoload/remote/define_spec.lua | 365 +++++++++++++++++++++ .../runtime/autoload/rpc/define_spec.lua | 365 --------------------- 13 files changed, 1003 insertions(+), 1003 deletions(-) create mode 100644 runtime/autoload/remote/define.vim create mode 100644 runtime/autoload/remote/host.vim delete mode 100644 runtime/autoload/rpc/define.vim delete mode 100644 runtime/autoload/rpc/host.vim delete mode 100644 runtime/doc/external_plugin.txt create mode 100644 runtime/doc/remote_plugin.txt delete mode 100644 runtime/plugin/external_plugins.vim create mode 100644 runtime/plugin/rplugin.vim create mode 100644 test/functional/runtime/autoload/remote/define_spec.lua delete mode 100644 test/functional/runtime/autoload/rpc/define_spec.lua diff --git a/runtime/autoload/provider/python.vim b/runtime/autoload/provider/python.vim index 9ca81c35f4..53b984dfe2 100644 --- a/runtime/autoload/provider/python.vim +++ b/runtime/autoload/provider/python.vim @@ -11,11 +11,11 @@ let s:loaded_python_provider = 1 let s:plugin_path = expand(':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, []) +call remote#host#RegisterClone('legacy-python-provider', 'python') +call remote#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') + let s:host = remote#host#Require('legacy-python-provider') catch echomsg v:exception finish diff --git a/runtime/autoload/remote/define.vim b/runtime/autoload/remote/define.vim new file mode 100644 index 0000000000..dd2482998d --- /dev/null +++ b/runtime/autoload/remote/define.vim @@ -0,0 +1,248 @@ +function! remote#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 = ',' + elseif matchstr(a:opts.range, '\d') != '' + " -range=N, pass the count + let prefix = '' + endif + elseif has_key(a:opts, 'count') + let prefix = '' + endif + + let forward_args = [prefix.a:name] + + if has_key(a:opts, 'bang') + call add(forward_args, '') + endif + + if has_key(a:opts, 'register') + call add(forward_args, ' ') + endif + + if has_key(a:opts, 'nargs') + call add(forward_args, ' ') + endif + + exe s:GetCommandPrefix(a:name, a:opts) + \ .' call remote#define#CommandBootstrap("'.a:host.'"' + \ . ', "'.a:method.'"' + \ . ', "'.a:sync.'"' + \ . ', "'.a:name.'"' + \ . ', '.string(a:opts).'' + \ . ', "'.join(forward_args, '').'"' + \ . ')' +endfunction + + +function! remote#define#CommandBootstrap(host, method, sync, name, opts, forward) + let channel = remote#host#Require(a:host) + + if channel + call remote#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! remote#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, '[]') + 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, '[, ]') + elseif matchstr(a:opts.range, '\d') != '' + " -range=N, pass the count + call add(rpcargs, '') + endif + elseif has_key(a:opts, 'count') + " count + call add(rpcargs, '') + endif + + if has_key(a:opts, 'bang') + " bang + call add(rpcargs, ' == "!"') + endif + + if has_key(a:opts, 'register') + " register + call add(rpcargs, '') + endif + + call s:AddEval(rpcargs, a:opts) + exe s:GetCommandPrefix(a:name, a:opts) + \ . ' call '.s:GetRpcFunction(a:sync).'('.join(rpcargs, ', ').')' +endfunction + + +function! remote#define#AutocmdOnHost(host, method, sync, name, opts) + let group = s:GetNextAutocmdGroup() + let forward = '"doau '.group.' '.a:name.' ".'.'expand("")' + let a:opts.group = group + let bootstrap_def = s:GetAutocmdPrefix(a:name, a:opts) + \ .' call remote#define#AutocmdBootstrap("'.a:host.'"' + \ . ', "'.a:method.'"' + \ . ', "'.a:sync.'"' + \ . ', "'.a:name.'"' + \ . ', '.string(a:opts).'' + \ . ', "'.escape(forward, '"').'"' + \ . ')' + exe bootstrap_def +endfunction + + +function! remote#define#AutocmdBootstrap(host, method, sync, name, opts, forward) + let channel = remote#host#Require(a:host) + + exe 'autocmd! '.a:opts.group + if channel + call remote#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! remote#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! remote#define#FunctionOnHost(host, method, sync, name, opts) + let group = s:GetNextAutocmdGroup() + exe 'autocmd! '.group.' FuncUndefined '.a:name + \ .' call remote#define#FunctionBootstrap("'.a:host.'"' + \ . ', "'.a:method.'"' + \ . ', "'.a:sync.'"' + \ . ', "'.a:name.'"' + \ . ', '.string(a:opts) + \ . ', "'.group.'"' + \ . ')' +endfunction + + +function! remote#define#FunctionBootstrap(host, method, sync, name, opts, group) + let channel = remote#host#Require(a:host) + + exe 'autocmd! '.a:group + exe 'augroup! '.a:group + if channel + call remote#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! remote#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/remote/host.vim b/runtime/autoload/remote/host.vim new file mode 100644 index 0000000000..54a8bb3c41 --- /dev/null +++ b/runtime/autoload/remote/host.vim @@ -0,0 +1,243 @@ +let s:hosts = {} +let s:plugin_patterns = { + \ 'python': '*.py' + \ } +let s:remote_plugins_manifest = fnamemodify($MYVIMRC, ':p:h') + \.'/.'.fnamemodify($MYVIMRC, ':t').'-rplugin~' + + +" Register a host by associating it with a factory(funcref) +function! remote#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! remote#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! remote#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! remote#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(':p:h').'/nvim_plugin.py' +" call remote#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("")'}}, +" \ {'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! remote#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 remote#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 remote#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 remote#define#AutocmdOnHost(a:host, rpc_method, sync, name, opts) + elseif type == 'function' + let rpc_method .= ':function:'.name + call remote#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! remote#host#LoadRemotePlugins() + if filereadable(s:remote_plugins_manifest) + exe 'source '.s:remote_plugins_manifest + endif +endfunction + + +function! s:RegistrationCommands(host) + " Register a temporary host clone for discovering specs + let host_id = a:host.'-registration-clone' + call remote#host#RegisterClone(host_id, a:host) + let pattern = s:plugin_patterns[a:host] + let paths = globpath(&rtp, 'rplugin/'.a:host.'/'.pattern, 0, 1) + for path in paths + call remote#host#RegisterPlugin(host_id, path, []) + endfor + let channel = remote#host#Require(host_id) + let lines = [] + for path in paths + let specs = rpcrequest(channel, 'specs', path) + call add(lines, "call remote#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:UpdateRemotePlugins() + 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:remote_plugins_manifest) +endfunction + + +command! UpdateRemotePlugins call s:UpdateRemotePlugins() + + +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 remote#host#Register('python', function('s:RequirePythonHost')) +" }}} diff --git a/runtime/autoload/rpc/define.vim b/runtime/autoload/rpc/define.vim deleted file mode 100644 index e7817c5fac..0000000000 --- a/runtime/autoload/rpc/define.vim +++ /dev/null @@ -1,248 +0,0 @@ -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 = ',' - elseif matchstr(a:opts.range, '\d') != '' - " -range=N, pass the count - let prefix = '' - endif - elseif has_key(a:opts, 'count') - let prefix = '' - endif - - let forward_args = [prefix.a:name] - - if has_key(a:opts, 'bang') - call add(forward_args, '') - endif - - if has_key(a:opts, 'register') - call add(forward_args, ' ') - endif - - if has_key(a:opts, 'nargs') - call add(forward_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, '[]') - 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, '[, ]') - elseif matchstr(a:opts.range, '\d') != '' - " -range=N, pass the count - call add(rpcargs, '') - endif - elseif has_key(a:opts, 'count') - " count - call add(rpcargs, '') - endif - - if has_key(a:opts, 'bang') - " bang - call add(rpcargs, ' == "!"') - endif - - if has_key(a:opts, 'register') - " register - call add(rpcargs, '') - 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("")' - 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 deleted file mode 100644 index 9294dd92a8..0000000000 --- a/runtime/autoload/rpc/host.vim +++ /dev/null @@ -1,243 +0,0 @@ -let s:hosts = {} -let s:plugin_patterns = { - \ 'python': '*.py' - \ } -let s:external_plugins = fnamemodify($MYVIMRC, ':p:h') - \.'/.'.fnamemodify($MYVIMRC, ':t').'-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(':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("")'}}, -" \ {'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, 'external-plugin/'.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/Makefile b/runtime/doc/Makefile index a498223517..82fd921038 100644 --- a/runtime/doc/Makefile +++ b/runtime/doc/Makefile @@ -21,7 +21,7 @@ DOCS = \ digraph.txt \ editing.txt \ eval.txt \ - external_plugin.txt \ + remote_plugin.txt \ farsi.txt \ filetype.txt \ fold.txt \ @@ -142,7 +142,7 @@ HTMLS = \ digraph.html \ editing.html \ eval.html \ - external_plugin.html \ + remote_plugin.html \ farsi.html \ filetype.html \ fold.html \ diff --git a/runtime/doc/external_plugin.txt b/runtime/doc/external_plugin.txt deleted file mode 100644 index 2733bfbe62..0000000000 --- a/runtime/doc/external_plugin.txt +++ /dev/null @@ -1,136 +0,0 @@ -*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| -4. Plugin manifest |external-plugin-manifest| - -============================================================================== -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 its 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 about external plugins is with an example, so let's see -how a very useless python plugin looks like. This plugin exports a command, a -function and an autocmd. The plugin is called 'Limit', and all it does is -limit the number of requests 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("")', - 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 -< - -As can be seen, the plugin is implemented using pure python idioms(classes, -methods and decorators), the translation between these language-specific -idioms to vimscript occurs while the plugin manifest is being generated(see -below). - -Notice that the exported command and autocmd are defined with the "sync" flag, -which affects how Nvim calls the plugin: with "sync" the |rpcrequest()| -function is used, which will block Nvim until the handler function returns a -value. Without the "sync" flag, the call is made using a fire and forget -approach with |rpcnotify()|(return values or exceptions raised in the handler -function are ignored) - -To test the above plugin, it must be saved in "external-plugin/python" in a -'runtimepath' directory(~/.nvim/external-plugin/python/limit.py for example). -Then, the external plugin manifest must be generated with -`:UpdateExternalPlugins`. - -============================================================================== -4. External plugin manifest *external-plugin-manifest* - -Just installing external plugins to "external-plugin/{host}" isn't enough to -load them at startup. The `:UpdateExternalPlugins` command must be executed -every time an external plugin is installed, updated, or deleted. - -`:UpdateExternalPlugins` will generate the external plugin manifest, a special -vimscript file containing declarations for all vimscript entities -(commands/autocommands/functions) defined by all external plugins, with each -entity associated with the host and plugin path. The manifest can be seen as a -generated extension to the user's vimrc(it even has the vimrc filename -prepended). - -The manifest declarations are nothing but calls to the rpc#host#RegisterPlugin -function, which will take care of bootstrapping the host as soon as the -declared command, autocommand or function is used for the first time. - -The manifest generation step is necessary to keep editor startup fast in -situations where a user has external plugins with different hosts. For -example, imagine a user that has three plugins, for python, java and .NET -hosts respectively, if we were to load all three plugins at startup, then -three language runtimes would also be spawned which could take seconds! - -With the manifest, each host will only be loaded when required. Continuing -with the example, imagine the java plugin is a semantic completion engine for -java files, if it defines an BufEnter *.java autocommand then the java host -will only be spawned when java source files are loaded. - -If the explicit call to `:UpdateExternalPlugins` seems incovenient, try -to see it like this: Its a way to give IDE-like capabilities to nvim while -still keeping it a fast/lightweight editor for general use. It can also be -seen as an analogous to the |:helptags| facility. - -============================================================================== - vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/nvim_intro.txt b/runtime/doc/nvim_intro.txt index 177c906db6..8a82a09890 100644 --- a/runtime/doc/nvim_intro.txt +++ b/runtime/doc/nvim_intro.txt @@ -17,7 +17,7 @@ differentiate Nvim from Vim: 2. Job control |job-control| 3. Python plugins |nvim-python| 4. Clipboard integration |nvim-clipboard| -5. External plugins |external-plugin| +5. Remote plugins |remote-plugin| 6. Provider infrastructure |nvim-provider| ============================================================================== diff --git a/runtime/doc/remote_plugin.txt b/runtime/doc/remote_plugin.txt new file mode 100644 index 0000000000..ca7e763d1b --- /dev/null +++ b/runtime/doc/remote_plugin.txt @@ -0,0 +1,136 @@ +*remote_plugin.txt* For Nvim. {Nvim} + + + NVIM REFERENCE MANUAL by Thiago de Arruda + + +Nvim support for remote plugins *remote-plugin* + +1. Introduction |remote-plugin-intro| +2. Plugin hosts |remote-plugin-hosts| +3. Example |remote-plugin-example| +4. Plugin manifest |remote-plugin-manifest| + +============================================================================== +1. Introduction *remote-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 +remote 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 *remote-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 its registered plugins are required, keeping +Nvim startup as fast a possible despite the number of installed plugins/hosts. + +============================================================================== +3. Example *remote-plugin-example* + +The best way to learn about remote plugins is with an example, so let's see +how a very useless python plugin looks like. This plugin exports a command, a +function and an autocmd. The plugin is called 'Limit', and all it does is +limit the number of requests 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("")', + 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 +< + +As can be seen, the plugin is implemented using pure python idioms(classes, +methods and decorators), the translation between these language-specific +idioms to vimscript occurs while the plugin manifest is being generated(see +below). + +Notice that the exported command and autocmd are defined with the "sync" flag, +which affects how Nvim calls the plugin: with "sync" the |rpcrequest()| +function is used, which will block Nvim until the handler function returns a +value. Without the "sync" flag, the call is made using a fire and forget +approach with |rpcnotify()|(return values or exceptions raised in the handler +function are ignored) + +To test the above plugin, it must be saved in "rplugin/python" in a +'runtimepath' directory(~/.nvim/rplugin/python/limit.py for example). +Then, the remote plugin manifest must be generated with +`:UpdateRemotePlugins`. + +============================================================================== +4. remote plugin manifest *remote-plugin-manifest* + +Just installing remote plugins to "rplugin/{host}" isn't enough to +load them at startup. The `:UpdateRemotePlugins` command must be executed +every time a remote plugin is installed, updated, or deleted. + +`:UpdateRemotePlugins` will generate the remote plugin manifest, a special +vimscript file containing declarations for all vimscript entities +(commands/autocommands/functions) defined by all remote plugins, with each +entity associated with the host and plugin path. The manifest can be seen as a +generated extension to the user's vimrc(it even has the vimrc filename +prepended). + +The manifest declarations are nothing but calls to the remote#host#RegisterPlugin +function, which will take care of bootstrapping the host as soon as the +declared command, autocommand or function is used for the first time. + +The manifest generation step is necessary to keep editor startup fast in +situations where a user has remote plugins with different hosts. For +example, imagine a user that has three plugins, for python, java and .NET +hosts respectively, if we were to load all three plugins at startup, then +three language runtimes would also be spawned which could take seconds! + +With the manifest, each host will only be loaded when required. Continuing +with the example, imagine the java plugin is a semantic completion engine for +java files, if it defines an BufEnter *.java autocommand then the java host +will only be spawned when java source files are loaded. + +If the explicit call to `:UpdateRemotePlugins` seems incovenient, try +to see it like this: Its a way to give IDE-like capabilities to nvim while +still keeping it a fast/lightweight editor for general use. It can also be +seen as an analogous to the |:helptags| facility. + +============================================================================== + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/plugin/external_plugins.vim b/runtime/plugin/external_plugins.vim deleted file mode 100644 index 1c5fa1ab8e..0000000000 --- a/runtime/plugin/external_plugins.vim +++ /dev/null @@ -1,5 +0,0 @@ -if exists('loaded_external_plugins') || &cp - finish -endif -let loaded_external_plugins = 1 -call rpc#host#LoadExternalPlugins() diff --git a/runtime/plugin/rplugin.vim b/runtime/plugin/rplugin.vim new file mode 100644 index 0000000000..2b2d333738 --- /dev/null +++ b/runtime/plugin/rplugin.vim @@ -0,0 +1,5 @@ +if exists('loaded_remote_plugins') || &cp + finish +endif +let loaded_remote_plugins = 1 +call remote#host#LoadRemotePlugins() diff --git a/test/functional/runtime/autoload/remote/define_spec.lua b/test/functional/runtime/autoload/remote/define_spec.lua new file mode 100644 index 0000000000..53da47243c --- /dev/null +++ b/test/functional/runtime/autoload/remote/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": "@"}') + 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("")'}]]) + 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('remote#host#Register("busted", '..channel()..')') +end + +command_specs_for('remote#define#CommandOnChannel', true, channel) +command_specs_for('remote#define#CommandOnChannel', false, channel) +command_specs_for('remote#define#CommandOnHost', true, host, register) +command_specs_for('remote#define#CommandOnHost', false, host, register) + +autocmd_specs_for('remote#define#AutocmdOnChannel', true, channel) +autocmd_specs_for('remote#define#AutocmdOnChannel', false, channel) +autocmd_specs_for('remote#define#AutocmdOnHost', true, host, register) +autocmd_specs_for('remote#define#AutocmdOnHost', false, host, register) + +function_specs_for('remote#define#FunctionOnChannel', true, channel) +function_specs_for('remote#define#FunctionOnChannel', false, channel) +function_specs_for('remote#define#FunctionOnHost', true, host, register) +function_specs_for('remote#define#FunctionOnHost', false, host, register) diff --git a/test/functional/runtime/autoload/rpc/define_spec.lua b/test/functional/runtime/autoload/rpc/define_spec.lua deleted file mode 100644 index a85ba4d37b..0000000000 --- a/test/functional/runtime/autoload/rpc/define_spec.lua +++ /dev/null @@ -1,365 +0,0 @@ -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": "@"}') - 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("")'}]]) - 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) -- cgit