

Writing Neovim plugins in Rust
source link: https://blog.usejournal.com/a-detailed-guide-to-writing-your-first-neovim-plugin-in-rust-a81604c606b1
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Writing Neovim plugins in Rust
A bit of background
Feel free to move to the example code, to avoid my ramblings.
Plugins have always been at the very center of the Vim experience. Traditionally, you were restricted to writing plugins in VimScript, or a small set of supported languages like Python or Ruby.
Between all the paradigm shifts that Neovim brought about, the ones that got me most excited were asynchronicity as a first-class citizen and an RPC API via msgpack, which is a binary data serialization format. This meant you could power Neovim plugins in any language that spoke msgpack. This idea seems to have caught on, as modern editors like Xi do this as well. Furthermore, since there are already ready-made client libraries, which means for most cases, you don’t even have to be aware of the msgpack implementation details! For someone like me who isn’t a VimScript guru, this was a very welcome change.
I recently wrote a plugin in Rust to control the Spotify desktop app for MacOS and find lyrics from within Neovim and found it to be a neat experience overall, and that’s coming from someone who isn’t a Rust expert. This post is a summary of all the research and discovery that happened during that process.
The overarching idea is fairly simple and can be summarized in two steps:
- Write a Rust application that can receive RPC messages from Neovim and “do stuff”. For example, Neovim sends a “get_current_song” RPC and the Rust application will query Spotify for the current song and echo it to the Neovim command line.
- Write the Neovim “glue” code. This is a small bit of VimScript, that does two things. First, it spawns a “job” using your Rust binary. A “job” is Neovim speak for attaching a remote plugin to an instance. Secondly, it attaches specific Neovim commands to RPC invocations. For example, the
:SpotifyCurrentSong
command will send an RPC message called “get_current_song” to your Rust code.
Note that you can substitute Rust for any compiled language, say Go, and still follow the same two steps. Of course, the client libraries you end up using will have idiosyncracies that you need to work with.
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 theEventHandler
state — that would get us into issues with the borrow checker since theNeovim
struct that we subsequently created required thesession
to be “moved” into it. Fortunately, theNeovim
struct that we created exposes thesession
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 ajob-id
. In true C-style, this value will be0
or-1
if the job cannot start. Otherwise, thisjob-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 callrpcnotify
with theadd
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 usingstr2nr
. 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 yourinit.vim
Plug '/path/to/neovim-calculator'
If all went well, you should have the two commands working!
Troubleshooting
There is zero visibility in this entire process unless you do it manually.
To debug on the Rust side, the easiest way is to create a file, and check for an environment variable such as NVIM_RUST_LOG_FILE
then write to it the same way you’d write logs in general. Read by tail -f
ing the file.
On the Vim side, use echo
and echoerr
. Also, keep your fingertips enthusiastic to jump to help documentation with :h
.
Next steps
As it stands, our plugin cannot be installed by the general public because they wouldn’t have the binary to connect to. An option is to provide a shell script that downloads the pre-built binary from your GitHub releases, falling back to manual Cargo builds if needed. If you want a reference, consult the install script for the Spotify plugin I made. Of course, you would have to set up your pipeline to cross-compile and release to GitHub. I leave it as an exercise to you, the reader.
Conclusion
You can extend the concepts above to any compiled language because the VimScript side stays the same, and there are equivalent client libraries for several platforms. Hopefully, that was a decent guide to getting a Neovim plugin working via RPC and it helps someone out! (looking at you, future self!)
📝 Read this story later in Journal.
🗞 Wake up every Sunday morning to the week’s most noteworthy Tech stories, opinions, and news waiting in your inbox: Get the noteworthy newsletter >
Recommend
-
150
Looking for testers - latest feature of the neovim version managerRecently I...
-
71
What is ESLint? ESLint is an open source JavaScript linter that is used to find code that doesn't adhere to style guideline...
-
32
Recap: I've been on a life-long quest to custom Vim to the point where it'd become impossible to know where the editor stops and the man begins. I experimented with Vim's embedded Perl interperter and created a [plugin...
-
65
In the beginning, there was Vim Script (also known as VimL). All Vim configuration, plugins, tweaks and hacks went through that configuration DSL with a sprinkling of flow control. VimL will take you wherever you w...
-
10
Plugins in Rust: The TechnologiesA more in-depth look at Rust PDKsMay 17, 2021 · 23 min · Mario Ortiz ManeroTable of Contents1. Introduction
-
18
I was curious if it would be possible to write a lua plugin in pure rust. It turns out this is quite straightforward. TLDR: You can use rust with a library like mlua to compile...
-
162
Awesome Neovim Collections of awesome Neovim plugins. Mostly targeting Neovim specific features. Contents Wishlist Have a problem a plugin can solve? Add it to...
-
11
Neovim 0.5: Lua, built in LSP, Treesitter and the best plugins for 2021 13.07.2021 0 comments 237 days since...
-
9
Writing like a pro with vale & neovim 📅️ Published: May 26, 2022 • 🕣 3 min read 📌 vim ...
-
7
Posted Aug 15, 2022 Updated Aug 18, 2022
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK