Hacking on Neovim Plugins
I’m having fun. It may seem mindless, pointless, or even counter-productive. But it’s not. It’s fun!
It all started with me optimizing my nvim
start times. Though I used vi
or vim
basically forever, and used nvim
since it started in 2014, I never really worried about optimizing startup times. But, truth is, nvim
is doing much more today than it ever did in the past, so maybe performance is more critical now.
I already used LazyVim as my starting point. Now I wanted to trim down the number of plugins I was using, maximize my lazy loading, and generally streamline my configuration. And that led me into hacking out a few really simple plugins.
But before I dive into the plugin stuff, I want to talk a bit about why I chose LazyVim awhile ago.
Contents
LazyVim?
LazyVim is a pre-configured nvim
setup based on the lazy.vim plugin manager.
I use LazyVim because it does most of the complex configuration for me, and yet it’s still easily customizable. So, out of the box, I get an awesome configuration for:
- all the languages I need (primarily rust and tex)
- a full debugger interface
- automatic testing
- LSP support, auto-completion, snippets
- Codeium completions
- formatting
- linting
- really nice UI additions and tweaks
- it’s fast!
In other words, it’s actually a true IDE. To me “true” here means it must include a debugger, at minimum.
And, you don’t have to use all that. It’s easy to configure. If you don’t need something, you just turn it off. If something’s missing, you can add it. Overall, it’s the best nvim
plugin manager and default configuration I’ve used.
Optimizing Startup
I always felt like my nvim
loaded quickly enough. I didn’t need to optimize anything. But then I thought, “hey, I’m not really taking advantage of lazy.vim
’s lazy loading support here”, and figured I’d see how far I could push it.
Most of my startup optimization came from configuring as many of my custom loaded plugins as possible to load lazily. This consists of setting lazy = true
and then either specifying the event to keystrokes to load on. For example, here’s how I’m loading nvim-origami and pretty-fold.nvim, so they won’t load until nvim
fires the BufReadPost
event.
return {
{
"chrisgrieser/nvim-origami",
dependencies = { "nvim-treesitter/nvim-treesitter" },
lazy = true,
event = "BufReadPost", -- later or on keypress would prevent saving folds
init = function()
vim.opt.foldenable = true
vim.opt.foldmethod = "expr"
vim.opt.foldexpr = "nvim_treesitter#foldexpr()"
vim.opt.foldnestmax = 3
vim.opt.foldminlines = 1
end,
opts = {},
},
{
"anuvyklack/pretty-fold.nvim",
lazy = true,
event = "BufReadPost",
opts = {},
},
}
Here’s how I load vim-scratch when the nvim
command Scratch
is called:
return {
"duff/vim-scratch",
lazy = true,
cmd = "Scratch",
}
And finally, here’s how I make yazi load via yazi.nvim on a keystroke:
return {
"DreamMaoMao/yazi.nvim",
lazy = true,
dependencies = {
"nvim-telescope/telescope.nvim",
"nvim-lua/plenary.nvim",
},
keys = {
{ "<leader>fy", "<cmd>Yazi<CR>", desc = "Toggle Yazi" },
},
}
Results
After trimming out lots of unnecessary plugins and making as many plugins as possible load lazily, my nvim
starts in under 90ms now (see screenshot below). Before this it was in the 250ms-300ms range.
As I said above, I don’t need this. But it’s very pleasing.
Writing Plugins
As I went through all the plugins I was using, I evaluated not only whether I really needed them, but also whether they were developed for nvim
(instead of just vim
) and played nicely with lazy loading.
thethethe.nvim
One of those plugins was vim-autocorrect. This is a plugin that automatically corrects common typos like “teh” into “the” by use of vim
abbreviations (iabbrev
). But, it was designed for vim
and written in vimscript
. I wanted something in lua
and specific to nvim
.
And so, thethethe.nvim was born. I converted the dictionary to lua
and deferred its loading, so it wouldn’t delay startup.
Now the dictionary looks like this:
return [[
adn:and
teh:the
-- ... thousands more words here
]]
In the initialization code, I wrote a function to load this dictionary into iabbrev
s, which I’m able to defer calling like this:
-- execute the function after config.delay_ms
vim.defer_fn(load_abbreviations, config.delay_ms)
At the end, I can lazy load my own plugin like this:
return {
"https://git.sr.ht/~swaits/thethethe.nvim",
lazy = true,
event = "VeryLazy",
opts = {},
}
Neat!
zellij-nav.nvim
Next up, in the process of switching from tmux to zellij, I wanted a way to easily move both between windows inside nvim
, and then also move from an nvim
pane in zellij to another zellij pane. I found a few plugins doing this, but I found them to be needlessly complex.
So I set out to create the smallest, simplest thing that worked. I ended up with 43 whole lines of lua
code. Yes, this is the entire plugin. It’s short enough to include here in its entirety:
local M = {}
local function nav(short_direction, direction)
-- get window ID, try switching windows, and get ID again to see if it worked
local cur_winnr = vim.fn.winnr()
vim.api.nvim_command("wincmd " .. short_direction)
local new_winnr = vim.fn.winnr()
-- if the window ID didn't change, then we didn't switch
if cur_winnr == new_winnr then
vim.fn.system("zellij action move-focus " .. direction)
if vim.v.shell_error ~= 0 then
error("zellij executable not found in path")
end
end
end
function M.up()
nav("k", "up")
end
function M.down()
nav("j", "down")
end
function M.right()
nav("l", "right")
end
function M.left()
nav("h", "left")
end
-- create our exported setup() function
function M.setup(opts)
-- create our commands
vim.api.nvim_create_user_command("ZellijNavigateUp", M.up, {})
vim.api.nvim_create_user_command("ZellijNavigateDown", M.down, {})
vim.api.nvim_create_user_command("ZellijNavigateLeft", M.left, {})
vim.api.nvim_create_user_command("ZellijNavigateRight", M.right, {})
end
return M
And I can load it like this:
return {
{
"https://git.sr.ht/~swaits/zellij-nav.nvim",
lazy = true,
keys = {
{ "<c-h>", "<cmd>ZellijNavigateLeft<cr>", { silent = true, desc = "navigate left" } },
{ "<c-j>", "<cmd>ZellijNavigateDown<cr>", { silent = true, desc = "navigate down" } },
{ "<c-k>", "<cmd>ZellijNavigateUp<cr>", { silent = true, desc = "navigate up" } },
{ "<c-l>", "<cmd>ZellijNavigateRight<cr>", { silent = true, desc = "navigate right" } },
},
opts = {},
},
}
So satisfying!
scratch.nvim
Finally, a conversion of vim-scratch, which hasn’t been updated in over a decade, to lua.
There are fancier scratch buffer plugins out there. But this one works exactly the way I like. And now I have it in lua.
This one shows one of the benefits of lua over vimscript – that the code is much more understandable and easier to follow.
I use it like this:
return {
"https://git.sr.ht/~swaits/scratch.nvim",
lazy = true,
keys = {
{ "<leader>bs", "<cmd>Scratch<cr>", desc = "Scratch Buffer", mode = "n" },
{ "<leader>bS", "<cmd>ScratchSplit<cr>", desc = "Scratch Buffer (split)", mode = "n" },
},
cmd = {
"Scratch",
"ScratchSplit",
},
opts = {},
}
Closing
In the beginning, I said this was fun. That’s so true; it was very fun.
I also said it might seem pointless or even counter-productive. But was it?
I don’t think so.
In the process of doing this, I learned some lua
, learned about nvim
plugins, and streamlined my entire coding and writing environment. But those are not the reasons this was a valuable use of my time.
It was valuable because it was fun!