Plugin System
NeoVim has two plugin mechanisms: the traditional runtimepath-based system inherited from Vim, and (as of 0.12) a built-in package manager called vim.pack. Most users interact through a third-party plugin manager, but understanding the underlying system matters.
Runtimepath Basics
NeoVim loads plugins by scanning directories on runtimepath. The key directories:
~/.config/nvim/ -- Your config (always on runtimepath)
plugin/ -- Lua/Vim files loaded on startup
after/plugin/ -- Loaded after all other plugins
ftplugin/ -- Filetype-specific config
lua/ -- Your Lua modules (require-able)
~/.local/share/nvim/site/ -- User-installed packages
pack/*/start/*/ -- Auto-loaded packages
pack/*/opt/*/ -- Opt-in packages (load with :packadd)
When NeoVim starts, it:
- Sources
init.lua(orinit.vim) - Loads all
plugin/**/*.luafiles from everyruntimepathentry - Triggers
VimEnterautocommand
Pack Directory Structure
The pack/ convention (from Vim 8) organizes plugins:
~/.local/share/nvim/site/pack/
core/ -- Group name (arbitrary)
start/ -- Auto-loaded at startup
telescope.nvim/
nvim-treesitter/
opt/ -- Loaded on demand with :packadd
nvim-dap/
Each plugin in start/ gets its directories added to runtimepath automatically. Plugins in opt/ are only loaded when you call :packadd plugin-name.
vim.pack - Built-in Package Manager (0.12+)
NeoVim 0.12 adds vim.pack, a built-in Git-based package manager. No external plugin manager needed.
-- In your init.lua
vim.pack.add("neovim/nvim-lspconfig")
-- With options
vim.pack.add("nvim-telescope/telescope.nvim", {
version = "0.1.*", -- Semver constraint
opt = true, -- Don't load at startup
})
-- Load an opt plugin later
vim.cmd.packadd("telescope.nvim")
How it works internally (from runtime/lua/vim/pack.lua):
- Plugins are cloned to
~/.local/share/nvim/site/pack/core/opt/ - A lockfile at
~/.config/nvim/nvim-pack-lock.jsonpins exact commits vim.pack.add()withopt = false(default) immediately adds to runtimepath- Version constraints use semver matching against Git tags
- Updates are Git fetch + checkout operations
Key functions:
vim.pack.add(name, opts) -- Add a plugin
vim.pack.update(name?) -- Update one or all plugins
vim.pack.lock() -- Write current state to lockfile
vim.pack.list() -- List installed plugins
Warning:
vim.packis new in 0.12 (currently dev). It works but the API may change. If you need stability now, lazy.nvim or mini.deps are proven alternatives.
Third-Party Plugin Managers
The ecosystem has several popular managers that build on these primitives:
| Manager | Approach |
|---|---|
| lazy.nvim | Lazy-loading, UI dashboard, lockfile, most popular |
| mini.deps | Minimal, ~300 lines, no frills |
| packer.nvim | Deprecated, predecessor to lazy.nvim |
| vim-plug | Vimscript-based, still works, simple |
All of them ultimately put files on runtimepath or in pack/. The value-add is lazy loading, dependency resolution, and update management.
Plugin Loading Order
Understanding the load order helps debug conflicts:
1. init.lua runs
- vim.pack.add() calls queue plugins
- vim.g.*, vim.o.* settings applied
2. Plugins load (from runtimepath)
- pack/*/start/*/plugin/*.lua -- Start plugins
- ~/.config/nvim/plugin/*.lua -- Your plugin dir
3. Filetype detection
- vim.filetype.match() runs
- ftplugin/*.lua loads for matched filetype
4. after/ plugins load
- pack/*/start/*/after/plugin/*.lua
- ~/.config/nvim/after/plugin/*.lua
5. VimEnter fires
- Autocommands for VimEnter run
Tip: Put plugin configuration in
after/plugin/if it needs to override plugin defaults. Theafter/directory loads last, guaranteeing the plugin is already loaded.
Writing a Plugin
A minimal NeoVim plugin is just a Lua module:
my-plugin/
lua/
my-plugin/
init.lua -- require("my-plugin") loads this
plugin/
my-plugin.lua -- Auto-runs on startup (optional)
-- lua/my-plugin/init.lua
local M = {}
function M.setup(opts)
opts = opts or {}
-- Apply user options, set up keymaps, create commands
vim.api.nvim_create_user_command("MyPlugin", function()
M.run()
end, {})
end
function M.run()
print("Plugin is running")
end
return M
The setup() convention is not enforced by NeoVim - it’s a community pattern. The user calls require("my-plugin").setup({...}) in their config. Files in plugin/ run automatically without any user action.
Filetype Plugins
NeoVim detects filetypes through vim.filetype.match() (a Lua rewrite of the old Vimscript detection). The detection rules live in runtime/lua/vim/filetype.lua (94KB of patterns).
To add custom filetype settings:
-- ~/.config/nvim/ftplugin/go.lua
-- This file runs whenever a Go buffer opens
vim.bo.tabstop = 4
vim.bo.shiftwidth = 4
vim.bo.expandtab = false
Or via autocommands:
vim.api.nvim_create_autocmd("FileType", {
pattern = "go",
callback = function()
vim.bo.tabstop = 4
vim.bo.expandtab = false
end,
})
Both approaches work. ftplugin/ files are slightly cleaner for per-filetype config. Autocommands are more flexible (patterns, groups, conditional logic).