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