Skip to content

Commit

Permalink
Add basic remote module support (#107)
Browse files Browse the repository at this point in the history
See: neovim/neovim#27949

This includes a helper method for defining remote modules as well as an
acceptance spec to demonstrate their usage.

I chose to implement a new DSL class just for remote modules because the
existing plugin DSL is far too complicated for simple RPC handling. As
remote plugins are phased out, I expect to phase out and eventually
deprecate the existing plugin DSL.
  • Loading branch information
alexgenco authored Jun 10, 2024
1 parent a6d93c4 commit 7e7b687
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 27 deletions.
38 changes: 11 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions lib/neovim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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+.
#
Expand Down
42 changes: 42 additions & 0 deletions lib/neovim/remote_module.rb
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions lib/neovim/remote_module/dsl.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions spec/acceptance/remote_module_spec.vim
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions spec/acceptance/runtime/example_remote_module.rb
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions spec/acceptance/runtime/plugin/example_remote_module.lua
Original file line number Diff line number Diff line change
@@ -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 })

0 comments on commit 7e7b687

Please sign in to comment.