Writing Neovim plugins in Rust. (or, any other compiled language!)
By: Srishan Bhattarai
Tags: Programming, Rust, Plugins, Neovim, Vim

An example plugin: Calculator

Let’s explore what each of those steps looks like by creating a small plugin called Calculator. We will look at a simple way to organize your code — YMMV.

To see the finished product, refer to the following repositories on GitHub:

I’ve tried to be as verbose as possible, because there’s a good chance my future self will read this when I try to write a plugin again, and I would rather there be a step-by-step guide when that happens. Feel free to skim parts you don’t think are relevant to you.

As the self-explanatory name probably suggests, the Rust code will be responsible for performing the computations and echo-ing back results. For brevity, we will only create two commands, :Add and :Multiply . Each of these commands take two arguments and echo out the result to the command line. For example, :Add 2 3 will echo Sum: 5 and :Product 2 3 will echo Product: 6 .

Create a new Rust binary project using Cargo.

$ cargo new --bin neovim-calculator

Next, pull in the Neovim RPC client library for Rust, neovim_lib. The documentation is your friend here, keep it close. Add the dependency to your Cargo.toml file. This is the only dependency we’ll need for this simple exercise.

[dependencies]

neovim-lib = "0.6.0"

The primary responsibilities of the Rust code are to receive RPC messages, associate the right service handlers to each message, and if required, cause a change in the Neovim instance (for example, echo-ing back responses). Let’s decouple these ideas into two major structs: EventHandler and Calculator . The EventHandler acts effectively as a controller, receiving RPC events and responding back, and calling the right service methods while the Calculator struct is the “business logic”. Substitute this with whatever “stuff” your plugin does.

The main function is trivial. We discuss why we need the event handler to be mut a little later on.

fn main() {

let mut event_handler = EventHander::new(); event_handler.recv();

}

The calculator service

The easy bit first: the Calculator struct. The following code should be fairly self-explanatory. I leave the i32 vs. i64 debates to you.

The event handler service

Next, let’s write the EventHandler . First, it will hold an instance of the Calculator struct as a field. Next, It will embed a Neovim struct from the client library inside it. If you see the documentation, this struct implements the NeovimApi and NeovimApiAsync traits, which is where all the power lies.

Each Neovim struct instance is created with a Session struct which you can think of as an active Neovim session. The Neovim struct is just a handle to manipulate this session. There are various ways to create a Session. You could use Unix sockets using $NVIM_LISTEN_ADDRESS or connect via TCP. I will let you figure out what suits your needs the best, but the simplest, in my opinion, is to attach to the “parent” instance.

Typed RPC messages using Rust’s enums

Finally, we can get to handling events via the recv() method. A small precursor to complete before that: RPC messages are received as strings by the neovim_lib library. But, we can map each of these strings into an enum to provide us with all the benefits of Rust’s enum types like exhaustive matching. We implement the From trait, to convert between an owned String and a Messages enum.

Subscribing to RPC messages

There are several ways to subscribe to RPC events coming from Neovim, all of which involve starting an event loop using the session we created earlier. Again, you might want to read the documentation to know which one is the best for you, but for our purposes, start_event_loop_channel is perfect, because it gives back an MPSC Receiver which we can iterate over to receive events and the arguments with which the command was called. Creating this receiver requires recv() to take a &mut self , which is the reason our original main() function also specified event_handler as mut .

Note that we do not have a session as part of the EventHandler state — that would get us into issues with the borrow checker since the Neovim struct that we subsequently created required the session to be “moved” into it. Fortunately, the Neovim struct that we created exposes the session as a public field which we can use.

Converting between Neovim and Rust data types

Almost there! Next, we need to serialize the values to the right Rust data types and invoke the calculator functions. Enter the Value enum. We are only interested in the Integer variant but you know what to do for your particular use case. From the same document, we see that we can use the as_i64 impl to get an Option<i64> from the Value . We’ll simply unwrap() the Option for now, but this is a good spot to validate user input. With a bit of idiosyncratic iterator usage, the serialization is pretty declarative.

Note that it is EXTREMELY easy to get into a rabbit hole of trial and error where data types from Neovim don’t serialize into the right Rust data types. This is an issue in general with cross-language RPC. We will take special care in our Neovim code to ensure that we are always passing integers.

Executing arbitrary Neovim commands

Finally, we will echo back the responses to the Neovim instance. This is done by the command method on the NeovimApi trait. Again, forgive the unwrap s.

Neovim “glue” code

We’re done with the Rust code! Let’s look at the Neovim “glue” to attach commands to specific RPC by diving into a little bit of VimScript.

Create a plugin/neovim-calculator.vim file that will load up when anyone installs this plugin. While you can just copy paste the final result and only a few tweaks will get your plugin running, there are a few concepts to grok if you want to know what’s going on. As with the Rust code, documentation is your friend — run :h <command_or_function> to know what it does.

We first need to create a “job” using the jobstart function. We will start a job using our Rust binary and in RPC mode. The jobstart function gives us back a job-id . In true C-style, this value will be 0 or -1 if the job cannot start. Otherwise, this job-id identifies a unique “channel”. See :h jobstart() for more information. Channels are what allow message passing between the Neovim and the Rust process. Since we are in RPC mode, the rpcnotify function will help us send messages. Again, see :h rpcnotify() to learn more. Finally, we need to attach specific commands with specific rpcnotify invocations. For example, :Add should call rpcnotify with the add message, which is what the Rust code recognizes. Same goes for :Multiply

Note that the Neovim job-control API is fairly complex and powerful, and I leave it to you, the reader, to investigate the intricacies.

Connecting to the Rust binary

Alright, on to the code! First, let’s initialize a local variable calculatorJobId and set it’s value to zero, which if you have followed the steps above, will know that it is an error value. Next, we will try to connect to the binary using jobstart which is done by the initRpc function. If the calculatorJobId is still zero, or possibly -1, then we could not connect. Otherwise, we have a valid RPC channel.

Local variables and functions are initialized using the s: prefix. This way, the variable and function names do not pollute the global namespace.

Hook up commands to RPC invocations

After we conclude that we have a valid channel, we will configure the commands to their RPC invocations. This is done by the aptly named command! command.

There is quite a bit going on here.

Firstly, we added the call to configureCommands after we got the channel ID.

Next, configureCommands added two user-defined commands, :Add and :Multiply . The -nargs=+ attribute specifies that the commands each take one or more arguments.

We call the function s:add with <f-args> when we receive the :Add command. <f-args> just means that s:add will be called with the same arguments that :Add was called with. The same goes for :Multiply .

Finally, onto the s:add and s:multiply methods themselves. The function signature has a strange ... argument, which is VimScript speak for variadic arguments. Next, we extract the first and second argument from the user input using get . Lastly, we call rpcnotify with the right RPC identifier, and the two arguments.

Note that rpcnotify was explicitly given two arguments which were converted to numbers using str2nr . Not two strings, not a list of strings, not a list of numbers. Be wary of how you’re passing data in your RPC calls, so that it matches up with how you’re parsing it on the other side.

Running the plugin

And that’s the VimScript side of things! To try this plugin out, you have a couple of options.

Add this directory to your runtimepath . See :h runtimepath to know more. If you are using vim-plug , you can add the following line to your init.vim

Plug '/path/to/neovim-calculator'

If all went well, you should have the two commands working!