Returning to the use of globals for all neovim configuration would be a step backward for the Neovim plugin ecosystem, especially when another better and easier possibility exists.
Some History
Recently, there’s been a bit of a debate going on in the Neovim community around how we should structure our plugins. One of the arguments that’s been put forward has been that we should return to the older “vim” style of plugin configuration, i.e global variables that are picked up by our plugins at runtime to handle configuration.
This argument is a response to the current trend of .setup{}
being overloaded by being used for both initialization and configuration.
This makes it more difficult to decide when a plugin should be configured vs initialized.
It’s also difficult, it’s argued, to keep the loading order for plugins correct when one plugin depends on another.
These arguments are only relevant when taken in the context of poorly utilized plugin management, and poorly isolated plugin configurations.
But, this isn’t only the fault of the author.
Many well known Neovim users publish, and presumably use, terribly fragile, frankly bad configurations.
Many simply have lists of plugins to be installed (nominally using either Lazy.nvim or Packer) and then they either follow that with a large file of plugin configurations or they require
a collection of files, which is functionally identical.
These configs might look like:
If this how my configuration was structured, I would run to global config vars too (or possibly just switch to VSCode).
vim.g
is a well known convention and was the de-facto configuration mechanism for years.
But the relative benefits listed are either not convincing, or short sighted.
Backwards compatibility with Vim
Most neovim plugins are written entirely in Lua, making Vim compatibility a non-issue.
”Fewer errors”
It’s much harder to get feedback from plugins about mis-configuration issues. This leads to:
- Confusing errors
- Unintended behaviors when subtly wrong data is passed in
- Intended configuration being ignored, and default behavior continuing
Configuration is always present
If the creation of configuration data itself contains side effects (e.g. reading from a file), that side effect will happen regardless of if the plugin itself is loaded.
No breakage if the plugin is missing
This “benefit” is precisely what we want to avoid. Our configurations shouldn’t have orphaned global variables.
Global, mutable variables aren’t guaranteed to be side effect free, they just guarantee that you won’t know what the side effect is when you declare them.
None of this is to say, though, that there aren’t issues with the .setup{}
paradigm, even when used with the current state of the art plugin management.
Lazy.nvim, spooky action, and the problem with setup
Right now, Lazy.nvim is the best out there in terms of plugin management, in large part because it treats nearly all configuration as data1. Keybinds, filetypes, and events that can trigger a plugin to be loaded are all very simple data structures (usually lists of strings). This is how it’s able to live up to it’s name sake and effectively lazy-load plugins at just the right time. This core functionality becomes unwieldy, though, if we separate out plugin declaration and configuration, like in the above example.
In that world, it’s much harder to tell when a plugin has been loaded. And
while Lazy.nvim’s killer feature, the automatic loading of plugins whenever
they’re required, makes working with dependencies much more robust, it
doesn’t guarantee that your plugin will have the right configuration when
it’s loaded. Keeping plugin specification together with it’s declaration by
using Lazy.nvim’s config
and init
fields alleviates much of this
pain, but not all of it. A dependency, especially when optional, can have a
complicated relationship to its dependent, sometimes requiring subtle
changes to the dependent’s specification which have to be removed if the
dependency is ever removed2.
Even more difficult to manage are partial, pseudo dependencies3. These are plugins that aren’t required for their dependent to function, but do require a change in their dependents configuration to be used 4. Collocation helps in these types of plugin relationships, but since the “dependency” isn’t truly managing its configuration, collocation is broken.
The best way to handle this is to specify the partial dependencies in the dependent’s plugin spec in Lazy.nvim and then simply remember the link between them. Here’s an example from the configuration of cmp in LazyVim. Note, even though this represents the current state of the art (in my opinion), there are still numerous implicit dependencies, and the effects of many partial dependencies:
This config is very good for many reasons, but if we were to remove cmp-path, we’d also need to remove the source from cmp’s base config. This example is simple and straightforward enough to be almost completely harmless. But in a more complicated module, like the treesitter module, or the lsp config in LazyVim, it might become very difficult to manage those types of small changes.
A brighter future
Lazy.nvim currently gives us great tools to control configuration data in the
form of the opts
field. This field essentially represents the table that will
be passed to the plugin’s .setup()
function on load. This separation means
that we can not only split up the creation of plugin configuration from it’s
initialization, it also means that other plugins can potentially modify this
data before the plugin is initialized. Cmp sources can add themselves as
sources (even without using the existing require('cmp').register_source
function), and treesitter modules can manage their configuration sections in
the treesitter setup table. This helps with collocation but makes us even
more dependent on Lazy.nvim when there’s a much easier solution possible.
Split .setup()
in two
Tired:
Wired:
Where plugin.lua
looks something like this
This approach fully decouples configuration and loading, which let’s us
require('plugin')
without fear of suddenly loading a slow or computationally
expensive main file from our plugin. The config
function is cheap to call,
and can also house simple logic about handling default values and rules around
extension. Importantly, this also means that plugin authors will have to
create sensible defaults. By design, load()
will always be called with no
parameters, since it handles no configuration, and will only load the plugin
with the default configuration if config
is never called.
In the mean time
All of this relies heavily on moving the large, if agile, community of existing neovim plugins and configurations. But, I think there’s a way to grab this benefit before anyone’s migrated over to this new strategy.
Lazy.nvim has some really excellent tools to help us, all predicated on the philosophy of specifying our configuration in pure data as much as possible. Lazy.nvim also will merge together duplicate specifications for the same plugin. This means we can specify another spec entry for cmp that looks like this5:
This let’s us keep our lazy-loading goodness, and separates out configuration from initialization without resorting to unwieldy and hard to manage global state. We can also comfortably delete the entire spec containing cmp-path without breaking our default cmp config at all. There’s a little more mental overhead involved with this approach, but it very well might be worth it in some cases.
EDIT:
Originally I also included a half-baked idea about a new field that Lazy.nvim could add, that I called
modifies
, that represents this sort ofextension
relationship. I removed it and added this after thinking more about it, and reading Folke’s comment on it5.The original version though, can be found here.
Conclusion
I would be sad to see us return to the global configurations and brittle specs of the past, and I want to see this community move toward more robust configurations. Enough posts litter our forums about configurations breaking for seemingly no reason. Too many configurations that started life simple and reliable become unreadable and brittle. Configuration doesn’t need to be so hard.
Footnotes
-
A great example of this style of configuration is LazyVim, and while I think more people should make their own configs from scratch, this is the one to start with if you want some inspiration. ↩
-
Common examples would be the way treesitter’s modules, cmp’s plugins, or the many plugins that modify lspconfig’s behavior are configured. ↩
-
The majority of examples for these types of plugins are what Marc calls
extensions
. I think that’s a very good way of thinking about the problem, and would probably be worth focusing on as it’s own class of configuration problem. ↩ -
Thanks to Folke for this suggestion in a comment on reddit. ↩ ↩2