Porting a Vim9Script Plugin to Neovim with Lua

2025-10-11

I wrote MBNotes over Christmas last year with twin goals: solve a real problem I was having with existing note taking apps, and give me something to chew on. It has a few features that I really love:

In addition to this, I really took the time to put a bow on this, including a flamboyantly over the top help document. Then I switched to Neovim, and because I wrote this plugin entirely in Vim 9’s vim9script.

It’s admittedly pretty hipster to have written a full plugin in vim9script, but I regret nothing. I learned a lot about the Vim internals through that process, and Vim9Script had some lovely improvements. This was a holiday project for me, and it came together beautifully. It would be easier to backport this plugin to classic VimScript, but that would be strange to me. I didn’t dislike classic vimscript, but I have never been an expert in it, and it doesn’t feel like the best place to invest my learning energy. Besides which, my Neovim config is already completely written with Lua. Say what you will about me, I will commit to an ecosystem.

I’m writing this introduction before really embarking on the port. I know I want it in Lua, and I know I want to be a good platform citizen there. My first step is to find out what that means: what does a good Lua plugin look like? My plugin relies heavily on a lot of Vim functionality, but also on the Quarto integrations already offered. I remember at the time that they had different plugins available for Neovim than for Vim, so that’s worth checking out. Beyond that, my plugin consists of a fairly simple global initialisation for shortcuts and opening the daily note, as well as a custom filetype and some layered additions. It’s been a while since I looked at this code, so I should also give it an inspection to see what we are really dealing with.

All that considered, we are left with a to do list of sorts:

  1. Find out the best practices for Neovim plugin development in Lua.
  2. Look at the Quarto out-of-the-box experience for Neovim (it’s not out of the Neovim box, but out of the Quarto plugin box)
  3. Audit my own code to see what I need to port, what I can leave, what I need to rethink, and try to come up with a scaffold.

Best Practices for Lua Development in Neovim.

It’s worth being familiar with both Lua, and Lua in Neovim. Before attempting to write a Neovim plugin in Lua, you should at least have tried to port your configuration to Lua. To do that, I recommend :help lua-guide, and perhaps taking a look at my own Neovim configuration.

But once you’ve ticked those boxes, how best to proceed? Honestly, this was a nightmare. I found so many different boilerplates, all of which seemed aimed at people who knew both more and less than me. Underneath it all, they didn’t seem to provide anything super valuable or hard to reproduce, but equally, they didn’t help me decipher the different parts of what was going on.

Then I found the lua-plugin page on Neovim’s website. This looks like it should be accessible in Neovim’s help, but I couldn’t find it in my own installation’s docs. After a bit of sleuthing, I found out why: it’s new! This article was only introduced to the Git repository on 1st September of this year. This page is the lantern in the darkness. It cuts through so much internet noise. In particular:

Armed with this newfound clarity and an understanding of why I found this so hard three months ago, I feel ready to move on. I will need a Lua file in the plugin/ directory, and the rest of my code (that in Vim9script had lived in autoload/) goes into the lua/ directory.

quarto-nvim vs. quarto-vim

On first inspection, quarto-vim says that it “currently only handles syntax highlighting for qmd files”. quarto-nvim makes no such assertion, and seems to carry a lot more. It does, however, say that it “provides a lot of functionality through existing plugins”. That works for me, I just need to make sure I integrate with a superset of them to minimise complexity. Let’s check the list:

The next section talks about code runners for previewing output of code snippets. Again, this is a feature I implemented myself that I no longer need to. After looking at the options, I chose benlubas/molten.nvim as a good fit for me. It depends on Jupyter kernels, which I already have set up for use in Quarto.

Beyond that, the docs on this plugin are slim, referring to the author’s config for further reference. That doesn’t help me, I just want to know what the :QuartoPreview command does and whether I can use it. The only next step is to set all of the above up to the point that I have a good Quarto setup within Neovim, and then see what I’m working with.

Interlude: Image Support in Terminals (or, how this project resulted in me

switching my terminal emulator)

I never really paid much attention to this before, but I write all my blog posts in Neovim, and I sometimes include images. Furthermore, I have been using the Terminal a lot more on my PC Terrapin. So finally I’m ready to learn about the different ways images show up in Terminal emulators.

There are two primary graphics protocols I’m aware of: Kitty, and Sixel. Kitty was spun out from the terminal emulator of the same name, and after a perusal of the specification, I like it quite a lot. Sixel, on the other hand, is a lot more widely supported, a lot more straightforward, and offers much poorer performance. Rather than a protocol, it’s a bitmap graphics format that was introduced for dot-matrix printers in the 1980s, so it’s really classic tty stuff. The way it works is crazy, especially the way it sends colour data, which seems like an add-on.

This puts me into a predicament: I use iTerm2 on my Macs, and Foot on Linux. Both of these support Sixel images, but not Kitty. In terms of options, there are four that I can really consider here: