host.vim 6.9 KB

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