Writing a plugin for Neovim in Typescript
2019-10-21
Recently I switched editors from Spacemacs (a vim mode distribution of emacs) to Neovim. I made the change due to frustration with how complicated my editor was in emacs, and honestly the experience has been wonderful. That discussion is for another time however.
Having made the switch, I have been interested in playing around with Neovim's headline feature: the remote plugin API. The jist is that Neovim provides a general purpose API in the form of a MessagePack protocol. Messagepack is a binary format which allows remote process communication in an efficient and language agnostic way. To make life easier, the maintainers of Neovim have created a set of language integrations which set things up for you. I've decided to develop a plugin using the nodejs integration. I will capture here the basic template for how to get things working and some tips and tricks I learned along the way.
Plugin Format
Neovim's language client integrations are very particular about how and where you layout your plugin file structure. If the structure is not just so, Neovim won't know where your remote plugin is located. Further, so called remote plugins must be registered before you can use them which adds another layer of complexity.
I found it useful to use a plugin manager, and add my local plugin to it to ensure that vim knows that my plugin exists. I use Dein, but any vim plugin manager should work.
call dein#add('c:/dev/Projects/vim-balsamic')
With my custom plugin folder added, I created a folder structure matching this pattern:
Project Root > rplugin > node > Plugin Name > Javascript Project
- Project Root represents the name of the plugin we added to the plugin manager.
- rplugin indicates that this plugin has a remote script to run in the background.
- node indicates that the remote script is a nodejs project and that Neovim should use the Neovim client.
- Javascript Project represents the same name as above, but allows for multiple remote node scripts per project.
In the inner Javascript Project folder (in my case vim-balsamic), I made a simple nodejs project. In particular I use typescript to make life easier
qcK7y:dist\
0Oubx:lib\
Tyhqo:node_modules\
aSW1g:.gitignore
5IzLv:package.json
T7EH2:tsconfig.json
itiAC:yarn.lock
(Note: the weird characters before file/folder names are related to my plugin. I will discuss them soon.)
The package.json file must contain the path to the entry point script so that Neovim knows which file to run with
node.js. It is also important that the package depend on the neovim
package to enable the MessagePack communication.
Plugin Writing
The documentation for the Neovim node client can be found here. However I found it somewhat confusing. The cleanest method I found was to define a class and use attributes to hook everything up.
The plugin attribute indicates that this class should be used as a Neovim plugin and the dev: false
flag is passed to
prevent Neovim from reloading the script on every command.
Similarly the command attribute defines a method on the plugin class as representing a command with the named passed in. This exposes that function in Neovim for use. Lastly the constructor for the plugin class takes a Neovim argument which I store as a public property. This object contains all of the API methods and properties needed for interacting with the Neovim app.
This object can be passed to methods like the one above, and called using async await to do operations one after another efficiently.
Once the plugin is written or at least compiles, running the UpdateRemotePlugins
command in Neovim will run your
plugin and inspect it to figure out what commands are defined. This way the plugin can be run lazily instead of on
startup slowing down vim.
Debugging
Crucially it is difficult to really debug or understand what is going on in a remote plugin without some debugger
support. To make this happen, two steps are necessary. First a chrome browser with the Node.js V8 -- inspector Manager
must be running. This can be setup
here. Second, an
environment variable must be set to tell Neovim to enable nodejs debugging. This can be done with this vim command:
:let $NVIM_NODE_HOST_DEBUG = 1
which will set the NVIM_NODE_HOST_DEBUG environment variable. Then when any command is
run which triggers the nodejs plugin, the chrome window will pop the debugger window and attach to the running process.
This makes life significantly easier and removes a lot of the print debugging which would otherwise be necessary.
My Plugins
I have been working on a file explorer plugin for Neovim using the above techniques. Its not quite ready yet, but I plan on writing about it soon. Its changed the way I interact with files :)
Till tomorrow,
Kaylee