neovim: refactored config

April 2022 ยท 4 minute read

Roughly two years ago, I wrote about what Vim plugins I use. At that time I was using Neovim much as I had always used Vim before. Since then, Neovim has evolved and so has the ecosystem of plugins around it.

In late 2021, I finally refactored my vimrc into Lua - more specifically, writing it in Yuescript and compiling it into Lua. Lua is definitely one of my favorite languages, but yue has quickly become my quick-and-dirty scripting language of choice.

Yuescript is an evolution of leafo’s moonscript - actively maintained, updated, and the author frequently takes user feedback into account.

Warning? Note? IDK.

While I am not here to sell yue, all of my code snippets henceforth are written in yue, not Lua. Some of it is supported by moonscript, and some of it is beyond moonscript’s feature set.

Configuration

I won’t go over every aspect of my config. It should be easy enough to jump through. Really just wanted to dump info about the general layout.

Building

I like to keep it simple (because I’m stupid), so I have a very simple Makefile that I use for working on my configuration.

build:
	yue init.yue
	cd lua && yue .

clean:
	rm init.lua
	cd lua && find . -type f -name '*.lua' -delete

install:
	nvim --headless -c 'autocmd User PackerComplete quitall' -c 'PackerSync'

My workflow is generally: change something, make, restart vim or :PackerCompile. If new plugins have been added, I tend to quit and run make install.

init

I don’t set up anything other than plugins unless packer exists already. If I am bootstrapping a new install, only plugins are included and sync’d.

-- init.yue
needs_bootstrap = false
with vim.fn
  p = (.stdpath "data") .. "/site/pack/packer/start/packer.nvim"
  if .empty(.glob p) > 0
    needs_bootstrap = .system {
      "git", "clone", "--depth", "1",
      "https://github.com/wbthomason/packer.nvim", p
    }

-- lua/plugins.yue, inside of `packer.setup`
-- will quit vim after plugins have been installed & packer has finished its compile
      if needs_bootstrap
        -- basically used to "pause" and remind me it'll quit
        vim.fn.input "bootstrapping, press any key (exit after)"
        vim.api.nvim_create_autocmd { "User PackerCompileDone" }
          command: "qall"
        require("packer").install!
        require("packer").compile!

Layout

I personally prefer to separate things into small chunks wherever possible, so my setup looks a bit wild at first glance.

lua/
   macros.yue

   plugins.yue
      (list of plugins to include)
   plug/
      (plugin specific configs)

   settings.yue
   keymaps.yue

   nvim_lsp.yue
      (I plan on refactoring this for per-lang settings)

   ft.yue
      (imports ft specific configs)
   ft/
      (filetype specific configs)

I require/exec these in order, so:

Each of these is essentially it’s own module that has a setup function. The simplest one is ft:

types =
  * "yue"
  * "go"
  * "cpp"
  * "lua"

export default {
  setup: ->
    require "ft.#{ft}" for _, ft in ipairs types
}

This allows me to separate the configuration from execution. A simple import (require in standard lua) won’t trigger a bunch of errors in a bootstrap scenario.

Per-Plugin Configuration

Thankfully, the majority of plugins I use have excellent defaults and don’t require much additional configuration.

Exactly like my use of top-level initialization, each plugin configuration exports a setup function that will be called, separating the actual config from the execution of it for the plugin.

As an example, here’s how I configure TimUntersberger/neogit.

-- neogit.yue
import "macros" as { $ }

cfg =
  kind: "split"
  integrations:
    diffview: true

export default {
  setup: (...) ->
    ng = require "neogit"
    
    ng.setup cfg

    $nosilent '<leader>g', '<cmd>Neogit<CR>'
}

As you can see, the configuration is outside of the actual setup, which subjectively looks nicer and lets it lean left as much as possible.

Additionally, keybinds are specified at execution time, so if a plugin is not loaded, it doesn’t try to map something that doesn’t exist.

It’s not always possible, as some things rely on functions provided by a package, i.e. hrsh7th/cmp - I set up actual mappings during setup, as otherwise it’s possible for cmp to not be installed.

cfg =
  snippet:
    expand: (args) ->
      require("luasnip").lsp_expand args.body
  sources:
    * name: 'nvim_lsp'
    * name: 'buffer'

export default {
  setup: (...) ->
    import "cmp" as cmp

    -- cmp is available, set up mappings here
    cfg.mapping =
      '<C-d>': cmp.mapping.scroll_docs -4
      '<C-f>': cmp.mapping.scroll_docs 4
      '<C-Space>': cmp.mapping.complete!
      '<Tab>': cmp.mapping cmp.mapping.select_next_item!, { 'i', 's' }
      '<C-e>': cmp.mapping.close!
      '<CR>': cmp.mapping.confirm
        select: true
        behavior: cmp.ConfirmBehavior.Replace
      '<Up>': cmp.mapping cmp.mapping.select_prev_item!, { 'i', 's' }
      '<Down>': cmp.mapping cmp.mapping.select_next_item!, { 'i', 's' }

    cmp.setup cfg
}

;wq

That’s it for now. Honestly not even sure what I was originally going for by writing this, but now I’m bored. Bye!