1let s:hosts = {} 2let s:plugin_patterns = {} 3let s:plugins_for_host = {} 4 5" Register a host by associating it with a factory(funcref) 6function! remote#host#Register(name, pattern, factory) abort 7 let s:hosts[a:name] = {'factory': a:factory, 'channel': 0, 'initialized': 0} 8 let s:plugin_patterns[a:name] = a:pattern 9 if type(a:factory) == type(1) && a:factory 10 " Passed a channel directly 11 let s:hosts[a:name].channel = a:factory 12 endif 13endfunction 14 15" Register a clone to an existing host. The new host will use the same factory 16" as `source`, but it will run as a different process. This can be used by 17" plugins that should run isolated from other plugins created for the same host 18" type 19function! remote#host#RegisterClone(name, orig_name) abort 20 if !has_key(s:hosts, a:orig_name) 21 throw 'No host named "'.a:orig_name.'" is registered' 22 endif 23 let Factory = s:hosts[a:orig_name].factory 24 let s:hosts[a:name] = { 25 \ 'factory': Factory, 26 \ 'channel': 0, 27 \ 'initialized': 0, 28 \ 'orig_name': a:orig_name 29 \ } 30endfunction 31 32" Get a host channel, bootstrapping it if necessary 33function! remote#host#Require(name) abort 34 if !has_key(s:hosts, a:name) 35 throw 'No host named "'.a:name.'" is registered' 36 endif 37 let host = s:hosts[a:name] 38 if !host.channel && !host.initialized 39 let host_info = { 40 \ 'name': a:name, 41 \ 'orig_name': get(host, 'orig_name', a:name) 42 \ } 43 let host.channel = call(host.factory, [host_info]) 44 let host.initialized = 1 45 endif 46 return host.channel 47endfunction 48 49function! remote#host#IsRunning(name) abort 50 if !has_key(s:hosts, a:name) 51 throw 'No host named "'.a:name.'" is registered' 52 endif 53 return s:hosts[a:name].channel != 0 54endfunction 55 56" Example of registering a Python plugin with two commands (one async), one 57" autocmd (async) and one function (sync): 58" 59" let s:plugin_path = expand('<sfile>:p:h').'/nvim_plugin.py' 60" call remote#host#RegisterPlugin('python', s:plugin_path, [ 61" \ {'type': 'command', 'name': 'PyCmd', 'sync': 1, 'opts': {}}, 62" \ {'type': 'command', 'name': 'PyAsyncCmd', 'sync': 0, 'opts': {'eval': 'cursor()'}}, 63" \ {'type': 'autocmd', 'name': 'BufEnter', 'sync': 0, 'opts': {'eval': 'expand("<afile>")'}}, 64" \ {'type': 'function', 'name': 'PyFunc', 'sync': 1, 'opts': {}} 65" \ ]) 66" 67" The third item in a declaration is a boolean: non zero means the command, 68" autocommand or function will be executed synchronously with rpcrequest. 69function! remote#host#RegisterPlugin(host, path, specs) abort 70 let plugins = remote#host#PluginsForHost(a:host) 71 72 for plugin in plugins 73 if plugin.path == a:path 74 throw 'Plugin "'.a:path.'" is already registered' 75 endif 76 endfor 77 78 if has_key(s:hosts, a:host) && remote#host#IsRunning(a:host) 79 " For now we won't allow registration of plugins when the host is already 80 " running. 81 throw 'Host "'.a:host.'" is already running' 82 endif 83 84 for spec in a:specs 85 let type = spec.type 86 let name = spec.name 87 let sync = spec.sync 88 let opts = spec.opts 89 let rpc_method = a:path 90 if type == 'command' 91 let rpc_method .= ':command:'.name 92 call remote#define#CommandOnHost(a:host, rpc_method, sync, name, opts) 93 elseif type == 'autocmd' 94 " Since multiple handlers can be attached to the same autocmd event by a 95 " single plugin, we need a way to uniquely identify the rpc method to 96 " call. The solution is to append the autocmd pattern to the method 97 " name(This still has a limit: one handler per event/pattern combo, but 98 " there's no need to allow plugins define multiple handlers in that case) 99 let rpc_method .= ':autocmd:'.name.':'.get(opts, 'pattern', '*') 100 call remote#define#AutocmdOnHost(a:host, rpc_method, sync, name, opts) 101 elseif type == 'function' 102 let rpc_method .= ':function:'.name 103 call remote#define#FunctionOnHost(a:host, rpc_method, sync, name, opts) 104 else 105 echoerr 'Invalid declaration type: '.type 106 endif 107 endfor 108 109 call add(plugins, {'path': a:path, 'specs': a:specs}) 110endfunction 111 112function! s:RegistrationCommands(host) abort 113 " Register a temporary host clone for discovering specs 114 let host_id = a:host.'-registration-clone' 115 call remote#host#RegisterClone(host_id, a:host) 116 let pattern = s:plugin_patterns[a:host] 117 let paths = nvim_get_runtime_file('rplugin/'.a:host.'/'.pattern, 1) 118 let paths = map(paths, 'tr(resolve(v:val),"\\","/")') " Normalize slashes #4795 119 let paths = uniq(sort(paths)) 120 if empty(paths) 121 return [] 122 endif 123 124 for path in paths 125 call remote#host#RegisterPlugin(host_id, path, []) 126 endfor 127 let channel = remote#host#Require(host_id) 128 let lines = [] 129 let registered = [] 130 for path in paths 131 unlet! specs 132 let specs = rpcrequest(channel, 'specs', path) 133 if type(specs) != type([]) 134 " host didn't return a spec list, indicates a failure while loading a 135 " plugin 136 continue 137 endif 138 call add(lines, "call remote#host#RegisterPlugin('".a:host 139 \ ."', '".path."', [") 140 for spec in specs 141 call add(lines, " \\ ".string(spec).",") 142 endfor 143 call add(lines, " \\ ])") 144 call add(registered, path) 145 endfor 146 echomsg printf("remote/host: %s host registered plugins %s", 147 \ a:host, string(map(registered, "fnamemodify(v:val, ':t')"))) 148 149 " Delete the temporary host clone 150 call jobstop(s:hosts[host_id].channel) 151 call remove(s:hosts, host_id) 152 call remove(s:plugins_for_host, host_id) 153 return lines 154endfunction 155 156function! remote#host#UpdateRemotePlugins() abort 157 let commands = [] 158 let hosts = keys(s:hosts) 159 for host in hosts 160 if has_key(s:plugin_patterns, host) 161 try 162 let commands += 163 \ ['" '.host.' plugins'] 164 \ + s:RegistrationCommands(host) 165 \ + ['', ''] 166 catch 167 echomsg v:throwpoint 168 echomsg v:exception 169 endtry 170 endif 171 endfor 172 call writefile(commands, g:loaded_remote_plugins) 173 echomsg printf('remote/host: generated rplugin manifest: %s', 174 \ g:loaded_remote_plugins) 175endfunction 176 177function! remote#host#PluginsForHost(host) abort 178 if !has_key(s:plugins_for_host, a:host) 179 let s:plugins_for_host[a:host] = [] 180 end 181 return s:plugins_for_host[a:host] 182endfunction 183 184function! remote#host#LoadErrorForHost(host, log) abort 185 return 'Failed to load '. a:host . ' host. '. 186 \ 'You can try to see what happened by starting nvim with '. 187 \ a:log . ' set and opening the generated log file.'. 188 \ ' Also, the host stderr is available in messages.' 189endfunction 190 191" Registration of standard hosts 192 193" Python/Python3 194call remote#host#Register('python', '*', 195 \ function('provider#pythonx#Require')) 196call remote#host#Register('python3', '*', 197 \ function('provider#pythonx#Require')) 198 199" Ruby 200call remote#host#Register('ruby', '*.rb', 201 \ function('provider#ruby#Require')) 202 203" nodejs 204call remote#host#Register('node', '*', 205 \ function('provider#node#Require')) 206 207" perl 208call remote#host#Register('perl', '*', 209 \ function('provider#perl#Require')) 210