Skip to content

Commit

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

This includes a helper module 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 committed May 30, 2024
1 parent a6d93c4 commit 93c8b33
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 0 deletions.
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
28 changes: 28 additions & 0 deletions spec/acceptance/runtime/plugin/example_remote_module.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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,
on_stderr = function(j, d, e)
io.stderr:write("error: " .. table.concat(d, '\n') .. "\n")
end
})

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 93c8b33

Please sign in to comment.