[POC] Ditch your plugins.lua: A Fully Modular, *slightly-nix-inspired*, Neovim Configuration Method

Modularity is something I’ve always considered top priority when it comes to my configs; being able to split things up into smaller pieces and have them spatially separated helps me understand/work with them. For this reason, I found typical Packer usage a little annoying, as you can only have one call to packer.start, and so regardless of where you put your configs, you are more or less forced to have every plugin specified in that call.

So, what to do about that?

Well, as a Nix/Home-Manager user, I’ve grown to very much appreciate HM’s module system and ‘passive’ (for lack of a better term) merging/overlaying of configs, and I got to thinking how I could possibly replicate that behavior in some way within my Neovim conf.

My approach

Define a layout for a “Module”.

A valid module file is a .lua file that returns a table with the following keys:

  • SETUP: A typical standalone lambda that should be run for setup/use of the module before packages are installed
  • PACKAGES: A suite of packages to be installed by packer
  • CONFIG: A typical standalone lambda that should be run after packages are installed
  • EXPORTS: A table of functions/values to be exported
  • RUN: Arbitrary lua code to be run at any time when invoked
  • If any of the keys are not found they should be appropriately interpreted as being empty

Activation flow:

  • ~merge module fields~
    1. run all setups
    2. merge packages into one table and then call use on all of them in a packer.setup call
    3. run all configs
    4. exports should be available to global access under their respective module names

The code required to make this happen is actually very small, I have some cruft and currently unused and unfinished functions, but the I’d estimate working LoC’s at below 50. See the activate_all function here: nixrc/module.lua at master · samuelludwig/nixrc · GitHub

I invoke my modules in my init.lua like so:

local f = require('lib.fun')
local mod = require('lib.module')
require('packer-bootstrap')

local prepend_mod_dot = function(x)
  return 'modules.' .. x
end

local mod_list = f.totable(f.map(prepend_mod_dot, {
  'core',
  'telescope-mod',
  'language_smartness',
  'vimwiki-mod',
  'lualine',
  'themes',
  'interface-helpers',
  'file-tree',
  'terminal',
}))

mod.activate_all(mod_list)

I have some cleaning up to do with my existing modules (and i still have normal configs mixed in that I need to move elsewhere), but you can see what I’m currently working with here: nixrc/modules/user/nvim/lua/dot/modules at master · samuelludwig/nixrc · GitHub

What I’d still like to do

I’ve been trying to think of a way to have a module “require” another, which would have it be implicitly added into the modules list before the requiring module is evaluated.

2 Likes

This seems neat! I did want to note, though, that you don’t have to specify all plugins in the same place with packer. If you use the startup function specification style, then, yes, all plugins must be specified in that one call. However, if you use the manual (i.e. call init and reset yourself) or table-based startup approaches, you can build your set of plugins across multiple files, based on conditions, etc.

Could you point out to an example for doing this? This is exactly what I’m looking for!

My dotfiles are a reasonable example of the manual style (dotfiles/plugins.lua at linux · wbthomason/dotfiles · GitHub). The table-based startup style is as simple as calling startup with your first argument being a table containing plugin specifications, e.g.:
packer.startup({{'tjdevries/colorbuddy.vim'}, config = { ... }, rocks = { ... }})

Thanks! I didn’t know you were the author of Packer :D.

1 Like