diff options
-rw-r--r-- | runtime/autoload/rpc/define.vim | 248 | ||||
-rw-r--r-- | runtime/autoload/rpc/host.vim | 242 | ||||
-rw-r--r-- | runtime/plugin/external_plugins.vim | 5 |
3 files changed, 495 insertions, 0 deletions
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/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() |