mirror of
https://github.com/neovim/neovim.git
synced 2026-04-20 14:25:32 +00:00
runtime: Add vimscript support for external plugins
External plugins(a.k.a msgpack-rpc plugins) are now supported through a library of vimscript functions that deals with: - Associating plugin host names(eg: python, ruby, go) with channel ids - Registration of external plugins - Definition of commands, autocmds and functions lazily implemented over msgpack-rpc
This commit is contained in:
248
runtime/autoload/rpc/define.vim
Normal file
248
runtime/autoload/rpc/define.vim
Normal file
@@ -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
|
||||
242
runtime/autoload/rpc/host.vim
Normal file
242
runtime/autoload/rpc/host.vim
Normal file
@@ -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'))
|
||||
" }}}
|
||||
5
runtime/plugin/external_plugins.vim
Normal file
5
runtime/plugin/external_plugins.vim
Normal file
@@ -0,0 +1,5 @@
|
||||
if exists('loaded_external_plugins') || &cp
|
||||
finish
|
||||
endif
|
||||
let loaded_external_plugins = 1
|
||||
call rpc#host#LoadExternalPlugins()
|
||||
Reference in New Issue
Block a user