‹ back home

You don't need a Neovim plugin manager

2026-01-08 #neovim

This article is a resurrected draft from 2022, and its core argument remains unchanged. Lua-specific portions apply only to Neovim, and everything else applies to both Neovim and Vim.

Let’s first understand the base mechanism which everything else builds upon: how Vim discovers and loads plugins…

The runtimepath list

Vim defines a runtimepath variable, a set of directories inside which it will search for plugins. For my Neovim installation, runtimepath includes the following:

The above list is, in part, defined as subdirectories of XDG_DATA_HOME and XDG_DATA_DIRS. Use :echo &runtimepath to view the exact values on your setup, or run :h runtimepath to see the documentation.

Inside any of these directories, Vim gives special treatment to a few special subdirectories:

Vim’s autoload mechanism

When Vim tries to execute a Vimscript function called autoload#filename#functionname(), Vim will search for a function called functionname in the file autoload/filename.vim. Vim will search for this file in any of the directories defined by runtimepath.

Essentially, placing files in the correct path in any of these directories will result in Vim lazy-loading the file when the function is first called, so placing code here is enough to lazy load it.

These are only loaded once, so if you have custom code that needs to be executed for HTML and XML files, you can include an ftplugin/html.vim and ftplugin/xml.vim which both autoload the same common file.

Neovim’s lua search paths

When using require('mymodule') in lua code, Neovim will search for the file mymodule in the lua directory inside any of the directories defined in runtimepath. Note that Lua support is entirely Neovim-specific.

The packadd command

Files in $HOME/.local/share/nvim/site/pack/*/start/* are loaded automatically. On the other hand, files in $HOME/.local/share/nvim/site/pack/*/opt/* are loaded manually via packadd. Generally, this is only needed for plugins which initialise pre-emptively (e.g.: by using plugin rather than ftplugin or autoload), but you only want to initialise them in specific scenarios.

Installing a plugin

Given the mechanisms described above, installing a plugin is just a matter of placing its source files into the correct directory, and plugins already include files in the correct layout.

Sometimes you want specific plugins to only initialise if you open a file of a given type. In the case of plugins which require explicit initialisation (e.g.: require('myplugin').setup()), the ideal place to do this is in ftplugin/python.lua (replacing python with whichever filetype you care about).

Vim plugins can be configured and lazy loaded with greater ease: they typically take configuration via global variables (e.g.: vim.g.myplugin_use_monochrome = 1 or let g:myplugin_use_monochrome = 1), and then lazy-load, for example, on a matching filetype. Such plugins only load when a matching file has been opened, lazy initialise, but allow configuration to be trivially defined in init.vim or init.lua. Neovim Lua plugins typically use a .setup() function to initialise, requiring additional scaffold if you want to delay loading them until necessary.

Plugin managers

Plugin managers basically fetch plugins (e.g.: git clone into the correct location). Some will place plugins into opt instead of start. Some plugin managers implement lazy-loading of plugins on demand, but this should not be necessary for plugins which properly implement lazy initialisation (e.g.: a Go plugin should ensure that it is auto-loaded by having its main entry point in ftplugin/go.vim or ftplugin/go.lua).

Plugin managers do provide certain conveniences, and there’s nothing wrong with using them, but it’s important to understand that they’re not a strict technical requirement.

In particular, when dealing with plugins which do not contemplate lazy-loading, plugin managers can help work around their initialisation and lazy-load them even if they’re not designed with this in mind. This can be achieved by using packadd, but requires additional manual setup.

Modern plugin managers have grown into orchestration frameworks. Most of that functionality compensates for plugin design, not editor limitations.

There are a few large alternatives to using plugin managers…

Distribution package manager

The same distribution channel which shipped Neovim likely has some plugins which can be installed the same way. Availability of plugins varies per distribution. The AUR and Nixpkgs in particular are known to include a large variety of plugins.

Tracking plugins with dotfiles

I track my dotfiles (including all my Neovim configuration) via git, so tracking plugins as git submodules has some natural synergy. Git itself can fetch and track plugins, and also gives me a clear overview if any plugins have local modifications which I may have forgotten about. Plugins are pinned to an exact version, and I can upgrade a plugin and its configuration in the same commit. Initial setup is also trivial.

The only manual step required is running :TSUpdate each time that the tree-sitter plugin is updated. Ideally, this shall change in future as the ecosystem starts to stabilise and we can rely on tree-sitter binaries installed through the usual channels.

In my dotfiles repository, I use the following command to add new plugins as submodules, installing them into the start directory so they are loaded automatically:

git submodule add \
    https://github.com/ibhagwan/fzf-lua \
    home/.local/share/nvim/site/pack/plugins/start/fzf-lua
git submodule add \
    https://github.com/lewis6991/gitsigns.nvim/ \
    home/.local/share/nvim/site/pack/plugins/start/gitsigns.nvim

This is the core of what a plugin manager ultimately does: place plugin sources into runtimepath. Having understood this, using a plugin manager becomes a matter of preference rather than necessity.

Have comments or want to discuss this topic?
Send an email to my public inbox: ~whynothugo/public-inbox@lists.sr.ht.
Or feel free to reply privately by email: hugo@whynothugo.nl.

— § —