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:
$HOME/.config/nvim/etc/xdg/nvim$HOME/.local/share/nvim/site$HOME/.local/share/nvim/site/pack/*/start/*/usr/local/share/nvim/site/usr/share/nvim/site/usr/share/nvim/runtime/usr/share/nvim/runtime/pack/dist/opt/netrw/usr/share/nvim/runtime/pack/dist/opt/matchit/usr/lib/nvim$HOME/.local/share/nvim/site/pack/*/start/*/after/usr/share/nvim/site/after/usr/local/share/nvim/site/after$HOME/.local/share/nvim/site/after/etc/xdg/nvim/after$HOME/.config/nvim/after
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:
plugin/: is loaded automatically.after/plugin/: is loaded automatically and last, overriding any defaults.ftplugin/: is lazy-loaded and executed when a file of a matching filetype is opened.autoload/: is lazy-loaded on demand. More on this below.lua/: its contents are loaded viarequire('modulename'). More on this below.
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.