Skip to content

Add basic remote module support #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 })
Loading