Open
Description
Problem
The "remote plugin" model currently is too complex, and 99% of the complexity provides very little benefit.
- Lifecycle is complicated, unnecessarily hard to test, troubleshoot, and explain to plugin authors.
- Every "remote plugin host" must implement 1000s of lines of code to solve the same problem in every API client. This is a pointless waste of time.
- Example: all of the code below is just for the "remote plugin" implementation in:
node-client
:go-client
:
- The benefits of the extra code are
- "multi-tenancy" (one node process for all node rplugins).
- "decorators" can be used in the remote module to define Nvim commands/autocmds.
- Example: all of the code below is just for the "remote plugin" implementation in:
- Too many moving parts, too many concepts.
- why is a "plugin host" needed?
- why can't I just call
vim.rplugin('node', 'path/to/index.js')
, then call functions defined in the remote process? - why do I need "decorators" (
@pynvim.plugin
) ? why can't I just use Lua to define commands/events inplugin/foo.lua
?@pynvim.command('MyCommand', …)
vs Luavim.api.nvim_create_user_command('MyCommand', function(...) vim.rpcrequest('MyCommand', ...))
:UpdateRemotePlugins
and the "manifest" are extra state that must be resolved, refreshed, and healthchecked.
Solution
- Reposition "remote plugins" as "remote modules". For example, a Node.js "remote plugin" is just a Node.js module that (1) imports "neovim", (2) can be started as a Nvim RPC client, and (3) handles requests from Nvim.
- Users no longer need to globally install
neovim-node-host
. - Client defines RPC method handlers via
setHandler()
. - Client calls
setup()
(placeholder name) which attaches and callsnvim_set_client_info()
with themethods
defined bysetHandler()
.
- Users no longer need to globally install
- Eliminate the "host" / "client" distinction. The client is the host (it can handle requests from Nvim).
- Drop "multi tenancy". Each plugin starts its own client ("host"). Plugins do not share a "host".
- Drop host-specific "sugar" for registering Nvim-side commands/autocmds (aka "bindings"). Plugins define commands/autocmds in Lua, like any other Nvim plugin. The commands call methods in the remote module.
Implementation
Implementation details
From doc/remote_plugin.txt
:
Plugin hosts ... take care of most boilerplate involved in defining commands, autocmds, and functions implemented over |RPC| connections. Hosts are loaded only when one of their registered plugins require it.
We can address the above use-cases as follows:
- Remote-plugins are just plain Lua plugins (
plugin/foo.lua
) that start API clients and call functions on the client.- Eliminates all the unnecessary complexity of trying to find
foo.js
/foo.py
/… plugin "mains".
- Eliminates all the unnecessary complexity of trying to find
- Make it easy from Lua to start any API client and treat it as a "remote host".
- One (API client) process per plugin.
- Future: Consider "sharing" client processes in the future. Out of scope for now.
- Eliminates all the redundant, per-platform (js/py/…) impls that implement a "remote host".
- Drop the concept of a "plugin host". We only need plain old API clients, not a "plugin host".
- Nvim Lua stdlib will provide (once—not in every API client!) an ergonomic interface to:
- start a "known" API client
- allow the caller to specify the path to the remote-plugin "module" or "main.
- define commands/autocmds/functions that call remote functions.
- "Decorators" are not need for this! If it's painful to do this in Lua, fix that once, in Nvim instead of "fixing" it N times in every API client!
- Examples (compare to
:help remote-plugin-example
):- go-client example
- pynvim example:
"Remote" Python code:"Local" Lua code:import pynvim def main(): pynvim.setHandler('command_handler', command_handler) pynvim.setHandler('autocmd_handler', autocmd_handler) pynvim.setHandler('function_handler', function_handler) # (placeholder name, TBD) # Attaches and calls `nvim_set_client_info()` with the `methods` defined by `setHandler()`. pynvim.setup() main()
local rplugin = vim.rplugin('python', 'path/to/init.py') vim.api.nvim_create_user_command('Cmd', function(args) vim.rpcrequest(rplugin.chan_id, 'command_handler', args.args, args.range) end, { nargs = '*' }) vim.api.nvim_create_autocmd({'BufEnter'}, { pattern = {"*.c", "*.h"}, callback = function(ev) vim.rpcrequest(rplugin.chan_id, 'autocmd_handler', ev.file) end }) function my_func(...) vim.rpcrequest(rplugin.chan_id, 'function_handler', {...}) end
- One (API client) process per plugin.
- With the above design...
- ELIMINATES:
- a fuckton of redundant documentation explaining crap like decorators, the "sync" flag, Remote plugin manifest,
remote#host#RegisterPlugin
, "bootstrapping" details. - a fuckton of code: neovim/src/plugin decorators/src/plugin neovim/src/host
rplugin/node/
runtimepath directory- "registration",
:UpdateRemotePlugins
, rplugin "manifest" remote#host#PluginsForHost
NvimPlugin.registerFunction
,NvimPlugin.registerAutocmd
,NvimPlugin.registerCommand
- a fuckton of redundant documentation explaining crap like decorators, the "sync" flag, Remote plugin manifest,
- STILL NEEDED:
- instead of
NvimPlugin
, the remote plugin uses the exact sameNvimClient
type that is returned byattach()
.
A remote plugin is just a library that operates on the same oldNvimClient
that any other API client operates on. provider#Poll()
detect()
require()
- instead of
- GAINS:
- The "bootstrapping" is now extremely obvioius and the plugin implementor fully controls when to call
vim.rplugin()
from Lua. vim.rplugin()
loads the plugin "main" and returns a channel:- find the platform interpreter (
node
,python
, …) - start the interpreter with a stdio RPC channel, targeting the plugin "main" file.
- The plugin "main" file is expected to
attach()
to stdio, and use the resultingNvimClient
to serve requests.
- The plugin "main" file is expected to
- calls
provider#Poll()
until success. - returns a channel id.
- find the platform interpreter (
- NO sugar for creating commands/functions/autocmds that connect to the remote plugin.
If creating commands/functions/autocmds is cumbersome we should fix that IN GENERAL, not only for remote plugins.vim.api.nvim_create_autocmd({'BufEnter'}, { pattern = {"*.c", "*.h"}, callback = function(ev) vim.rpcrequest(rplugin.chan_id, 'autocmd_handler', ev.file) end })
- The "bootstrapping" is now extremely obvioius and the plugin implementor fully controls when to call
- ELIMINATES:
FAQ
- How does a plugin avoid loading the interpreter (slow) on startup?
- To "share" an API client we could add an optional
ns
(namespace) parameter tovim.rplugin()
, then it could be called on-demand and re-uses the existing channel if found.- This doesn't need to be solved right now, can just use
jobstart()
.
- This doesn't need to be solved right now, can just use
- To "share" an API client we could add an optional
Related
- The above proposal has similarities to nvim-yarp cc @roxma
- But unlike nvim-yarp, we can go further and eliminate the concept of "registering" anything.
Work plan
- Add a util function that migrates legacy plugins using their "specs".
- Deprecate
:UpdateRemotePlugins
.- point to
:help remote-plugin-migrate
- ask user to report legacy plugins that haven't migrated yet: Which plugins use :UpdateRemotePlugins ? #29270
- point to
- Update the core API clients (implement
addHandler()
;setup()
in the client automatically callsnvim_set_client_info()
with themethods
defined byaddHandler()
; letjobstart()
invoke the module directly): - Update handling of
g:node_host_prog
, so it can point tonode
- Transitional phase:
neovim-node-host
will continue to be accepted; the path tonode
will be derived by inspecting the shebang inneovim-node-host
.
- Transitional phase:
- Update handling of
g:ruby_host_prog
, so it can point toruby
- Transitional phase:
neovim-ruby-host
will continue to be accepted; the path toruby
will be derived by inspecting the shebang inneovim-ruby-host
.
- Transitional phase:
- Update
:checkhealth
. - Update
:help remote-plugin
. - (Nvim 0.12) Remove old rplugin-related code
- (Nvim 0.12) Require
g:node_host_prog
to point tonode
, remove support forneovim-node-host
. - (Nvim 0.12) Require
g:ruby_host_prog
to point toruby
, remove support forneovim-ruby-host
.