Write custom Neovim plugins with ChatGPT
I wrote my first meaningful Neovim script with some help from our good friend Gippety
So this isn’t technically a plugin, it’s just a script, but I made my first non-trivial customization to Neovim this week and I want to show you how I did it.
I’ve been wanting for a while to make it simpler to get to the location of compilation errors while I’m working. Elm’s compilation tool, elm make, produces excellent errors, but I’ve been running it in a separate terminal window and it’s a real pain in the ass to run it, look at the filename, search for the file, open the file, look at the line number, and go to the line number. Especially since I know that “a list of compilation errors” is the canonical use for the quickfix list — I wanted to write something that would let me, with a single command, move my cursor to a compilation error.
Even though I knew this was technically possible, though, I wasn’t sure where to start. Which is where ChatGPT comes in. ChatGPT is really good at getting started and getting unstuck. It’s output always has a lot of problems, but it’s really good at getting me out of the “I know sort of what I want but not exactly” part of the project and into the “solving a series of specific technical problems” part of the project.
If you want to see the entire conversation with ChatGPT you can read the transcript. I won’t do a whole blow-by-blow, but I will share some techniques that I found useful while testing and modifying this script.
Subscribe for free to receive new posts and support my work.
Getting Started
The core prompt was basically this.
Write a plugin that will take the command `:ElmMake` and run `elm make ./src/Main.elm --report=json`, then if there are any items in the "errors" array it opens the list of "path" values in a quickfix window.
I also provided an example of the output of the report flag. It generally helps to give concrete examples of exactly what the input will look like and exactly what the output should look like, when you can.
It took a few tries to get here. I didn’t know about the report flag at first.
Testing Scripts
It’s initial attempt at the script had some bugs. It’s easy to test Lua scripts by loading the current file and then running whatever command they register.
:luafile %
Once I’ve got this loop in place it’s pretty simple to do some print-based debugging.
Simplifying Complicated Nonsense
Sometimes ChatGPT will do a really simple thing in a really complicated way. For example, when I was trying to remember that % trick, it tried to tell me that this was the best way to load a Luafile.
:lua vim.fn.execute(vim.fn.join(getline(1, '$')), 'silent')
Surely this can’t be right, I thought.
No I think there's a simpler way to refer to the file than that.
That got it to produce the result I wanted. It produces ludicrously complicated instructions for doing simple things pretty regularly so I recommend trying to get it to simplify frequently.
Populating all the fields in the quickfix list
The way you set the quickfix list is you build a table with a particular structure and you pass that table to setquickfix_list
local entry = {
filename = filename,
lnum = problem.region.start.line,
col = problem.region.start.column,
text = problem.title
}
table.insert(quickfix_list, entry)
vim.fn.setqflist(quickfix_list)
If you don’t provide the filename, lnum and I think the text the quickfix list won’t work correctly. (Though this was one thing that ChatGPT was able to help me with — I pasted in what I was seeing and asked it what was up.)
Automatically jump to the first error
This is the part that really makes this nice — instead of having to navigate in the quickfix list itself, this command just auto-jumps me to the first error.
vim.cmd("copen")
vim.cmd("cc")
Loading Lua files
Since this isn’t exactly a plugin (and I haven’t yet spent the time to figure out how to make “real” plugins) I load this script outside of my plugin manager, using Lua’s standard code loading mechanism.
require("elmmake")
This lets me store my script as “elmmake.lua” in the same directory as my “init.lua” script.
Then I bind the command to a key sequence with which-key.
local wk = require("which-key")
wk.register({
e = {
name = "Elm",
m = { "<cmd>ElmMake<cr>", "Elm Make" }
}
}, { prefix = "<leader>" })
Have you made your own Vim commands?
Let me know if you’ve made something recently, or are looking to make something in the future. I can share ‘em with the e-mail list if you want, but I’d also just generally like to see.