Hacking on Neovim Plugins

  /   4 minutes   /   tech   neovim  

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.

my nvim start menu

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 iabbrevs, 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!