diff --git a/README.md b/README.md index aa1fcf6..78ca11f 100644 --- a/README.md +++ b/README.md @@ -42,37 +42,21 @@ client = Neovim.attach_unix("/tmp/nvim.sock") Refer to the [`Neovim` docs](https://www.rubydoc.info/github/neovim/neovim-ruby/main/Neovim) for other ways to connect to `nvim`, and the [`Neovim::Client` docs](https://www.rubydoc.info/github/neovim/neovim-ruby/main/Neovim/Client) for a summary of the client interface. -### Plugins +### Remote Modules -Plugins are Ruby files loaded from the `$VIMRUNTIME/rplugin/ruby/` directory. Here's an example plugin: +Remote modules allow users to define custom handlers in Ruby. To implement a remote module: -```ruby -# ~/.config/nvim/rplugin/ruby/example_plugin.rb - -Neovim.plugin do |plug| - # Define a command called "SetLine" which sets the contents of the current - # line. This command is executed asynchronously, so the return value is - # ignored. - plug.command(:SetLine, nargs: 1) do |nvim, str| - nvim.current.line = str - end - - # Define a function called "Sum" which adds two numbers. This function is - # executed synchronously, so the result of the block will be returned to nvim. - plug.function(:Sum, nargs: 2, sync: true) do |nvim, x, y| - x + y - end - - # Define an autocmd for the BufEnter event on Ruby files. - plug.autocmd(:BufEnter, pattern: "*.rb") do |nvim| - nvim.command("echom 'Ruby file, eh?'") - end -end -``` +- Define your handlers in a plain Ruby script that imports `neovim` +- Spawn the script from lua using `jobstart` +- Define commands in lua using `nvim_create_user_command` that route to the job's channel ID + +For usage examples, see: -When you add or update a plugin, you will need to call `:UpdateRemotePlugins` to update the remote plugin manifest. See `:help remote-plugin-manifest` for more information. +- [`example_remote_module.rb`](spec/acceptance/runtime/example_remote_module.rb) +- [`example_remote_module.lua`](spec/acceptance/runtime/plugin/example_remote_module.lua) +- [`remote_module_spec.vim`](spec/acceptance/remote_module_spec.vim) -Refer to the [`Neovim::Plugin::DSL` docs](https://www.rubydoc.info/github/neovim/neovim-ruby/main/Neovim/Plugin/DSL) for a more complete overview of the `Neovim.plugin` DSL. +*Note*: Remote modules are a replacement for the deprecated "remote plugin" architecture. See https://github.com/neovim/neovim/issues/27949 for details. ### Vim Plugin Support diff --git a/lib/neovim.rb b/lib/neovim.rb index 8e201c7..6462e36 100644 --- a/lib/neovim.rb +++ b/lib/neovim.rb @@ -4,6 +4,7 @@ require "neovim/event_loop" require "neovim/executable" require "neovim/logging" +require "neovim/remote_module" require "neovim/version" # The main entrypoint to the +Neovim+ gem. It allows you to connect to a @@ -83,6 +84,14 @@ def self.attach_child(argv=[executable.path]) attach(EventLoop.child(argv)) end + # Start a remote module process with handlers defined in the config block. + # Blocks indefinitely to handle messages. + # + # @see RemoteModule::DSL + def self.start_remote(&block) + RemoteModule.from_config_block(&block).start + end + # Placeholder method for exposing the remote plugin DSL. This gets # temporarily overwritten in +Host::Loader#load+. # diff --git a/lib/neovim/remote_module.rb b/lib/neovim/remote_module.rb new file mode 100644 index 0000000..719f1d8 --- /dev/null +++ b/lib/neovim/remote_module.rb @@ -0,0 +1,42 @@ +require "neovim/client" +require "neovim/event_loop" +require "neovim/logging" +require "neovim/remote_module/dsl" +require "neovim/session" + +module Neovim + class RemoteModule + include Logging + + def self.from_config_block(&block) + new(DSL::new(&block).handlers) + end + + def initialize(handlers) + @handlers = handlers + end + + def start + event_loop = EventLoop.stdio + session = Session.new(event_loop) + client = nil + + session.run do |message| + case message + when Message::Request + begin + client ||= Client.from_event_loop(event_loop, session) + args = message.arguments.flatten(1) + + @handlers[message.method_name].call(client, *args).tap do |rv| + session.respond(message.id, rv, nil) if message.sync? + end + rescue => e + log_exception(:error, e, __method__) + session.respond(message.id, nil, e.message) if message.sync? + end + end + end + end + end +end diff --git a/lib/neovim/remote_module/dsl.rb b/lib/neovim/remote_module/dsl.rb new file mode 100644 index 0000000..1beb1f7 --- /dev/null +++ b/lib/neovim/remote_module/dsl.rb @@ -0,0 +1,30 @@ +module Neovim + class RemoteModule + # The DSL exposed in +Neovim.start_remote+ blocks. + # + # @api public + class DSL < BasicObject + attr_reader :handlers + + def initialize(&block) + @handlers = ::Hash.new do |h, name| + h[name] = ::Proc.new do |_, *| + raise NotImplementedError, "undefined handler #{name.inspect}" + end + end + + block&.call(self) + end + + # Define an RPC handler for use in remote modules. + # + # @param name [String] The handler name. + # @param block [Proc] The body of the handler. + def register_handler(name, &block) + @handlers[name.to_s] = ::Proc.new do |client, *args| + block.call(client, *args) + end + end + end + end +end diff --git a/spec/acceptance/remote_module_spec.vim b/spec/acceptance/remote_module_spec.vim new file mode 100644 index 0000000..14f7522 --- /dev/null +++ b/spec/acceptance/remote_module_spec.vim @@ -0,0 +1,13 @@ +let s:suite = themis#suite("Remote module") +let s:expect = themis#helper("expect") + +call themis#helper('command').with(s:) + +function! s:suite.defines_commands() abort + RbSetVar set_from_rb_mod foobar + call s:expect(g:set_from_rb_mod).to_equal('foobar') +endfunction + +function! s:suite.propagates_errors() abort + Throws /oops/ :RbWillRaise +endfunction diff --git a/spec/acceptance/runtime/example_remote_module.rb b/spec/acceptance/runtime/example_remote_module.rb new file mode 100644 index 0000000..5434fb8 --- /dev/null +++ b/spec/acceptance/runtime/example_remote_module.rb @@ -0,0 +1,11 @@ +require "neovim" + +Neovim.start_remote do |mod| + mod.register_handler("rb_set_var") do |nvim, name, val| + nvim.set_var(name, val.to_s) + end + + mod.register_handler("rb_will_raise") do |nvim| + raise "oops" + end +end diff --git a/spec/acceptance/runtime/plugin/example_remote_module.lua b/spec/acceptance/runtime/plugin/example_remote_module.lua new file mode 100644 index 0000000..701428b --- /dev/null +++ b/spec/acceptance/runtime/plugin/example_remote_module.lua @@ -0,0 +1,23 @@ +local chan + +local function ensure_job() + if chan then + return chan + end + + chan = vim.fn.jobstart({ + 'ruby', + '-I', 'lib', + 'spec/acceptance/runtime/example_remote_module.rb', + }, { rpc = true }) + + return chan +end + +vim.api.nvim_create_user_command('RbSetVar', function(args) + vim.fn.rpcrequest(ensure_job(), 'rb_set_var', args.fargs) +end, { nargs = '*' }) + +vim.api.nvim_create_user_command('RbWillRaise', function(args) + vim.fn.rpcrequest(ensure_job(), 'rb_will_raise', args.fargs) +end, { nargs = 0 })