Native Neovim config
Conventions for Neovim configs built on vim.pack, lsp/ and plugin/ with no
plugin manager framework. Requires Neovim >= v0.12.0.
References — read the one the task needs:
| File |
Covers |
references/loading-patterns.md |
vim.pack.add's load option, the three loading patterns, build hooks, profiling |
references/plugin-files.md |
File skeleton per pattern, _G.Config sharing, do/end blocks, ftplugin, option interfaces |
references/startup.md |
:h initialization step table, runtime directories, after/, exrc, help tags, standard paths |
This config's location
The native config lives at ~/.dotfiles/nvim-fredrik/ inside the dotfiles
repo. It is symlinked into place via GNU Stow:
~/.dotfiles/nvim-fredrik/ <- actual files (edit here)
~/.dotfiles/stow/shared/.config/nvim-fredrik -> ../../../nvim-fredrik (stow entry)
~/.config/nvim-fredrik -> ~/.dotfiles/stow/shared/.config/nvim-fredrik (stow result)
Launch it with NVIM_APPNAME=nvim-fredrik nvim. Apply stow symlinks after
changes from ~/.dotfiles/stow with packages=(shared "$(uname -s)"); host="$(hostname -s)"; [ -d "$host" ] && packages+=("$host"); stow --target="$HOME" --restow --no-folding --adopt "${packages[@]}". Neovim itself
comes from nixpkgs-unstable via home-manager (nix/shared/home/common.nix) --
binary at ~/.nix-profile/bin/nvim.
Architecture
No framework -- each directory has a single responsibility:
| Layer |
Directory |
Role |
| options |
lua/options.lua |
All vim.opt settings, required from init.lua |
| utility |
lua/ |
Shared Lua modules: lazyload.lua, merge.lua, fold.lua, toggle.lua, pickers, etc. |
| plugins |
plugin/ |
Self-contained plugin files: install + setup + keymaps |
| lang plugins |
plugin/lang/ |
Per-language plugin installs, custom filetypes, autocmds, and setup |
| editor settings |
ftplugin/ |
Per-filetype vim.opt_local (indent, wrap, conceal) |
| server config |
after/lsp/ |
All LSP server config tables (in after/ to override package defaults) |
~/.config/nvim-fredrik/
init.lua -- leader keys, require("options"), diagnostics, keymaps
lua/
lazyload.lua -- VimEnter/UIEnter deferred setup queues
merge.lua -- deep merge helper (appends+deduplicates lists, recurses dicts)
options.lua -- all vim.opt settings
dev.lua -- local dev plugin loader
... -- other utility modules (fold, toggle, pickers, icons, etc.)
lsp/ -- (unused; nvim-lspconfig provides base configs)
parser/ -- treesitter parser .so files (managed by nvim-treesitter)
colors/ -- custom colorschemes (loaded by :colorscheme)
snippets/ -- custom snippet files (loaded by blink.cmp)
ftplugin/ -- per-filetype editor settings (vim.opt_local)
plugin/
lang/ -- per-language plugins, custom filetypes, autocmds
blink.lua -- completion (VimEnter)
conform.lua -- formatting (VimEnter)
dap.lua -- debugging (deferred to first use)
lint.lua -- linting (VimEnter)
lsp.lua -- LSP enable + LspAttach keymaps (VimEnter)
lualine.lua -- statusline (VimEnter, sync)
mason.lua -- tool installation (VimEnter)
neotest.lua -- testing (deferred to first use)
<name>.lua -- other feature plugins (snacks, treesitter, oil, etc.)
after/
lsp/ -- all LSP server configs (overrides package defaults)
queries/<lang>/ -- treesitter query extensions (injections.scm, etc.)
syntax/<ft>.vim -- legacy syntax overrides/extensions
Notes on the layers:
lua/lazyload.lua provides on_vim_enter(fn, opts?) and on_ui_enter(fn, opts?) for queuing setup functions. Default is async (via vim.schedule());
{ sync = true } runs synchronously. Also provides on_override(fn) for
project-local overrides (runs after all VimEnter callbacks). Only lualine uses
{ sync = true }.
lua/merge.lua deep-merges: appends and deduplicates lists, recurses into
dicts, overwrites scalars. vim.NIL as a value removes a key.
lua/dev.lua loads a plugin from a local clone if it exists, otherwise
falls back to vim.pack.add().
plugin/ files are self-contained: vim.pack.add() -> setup -> keymaps.
Sourced alphabetically at step 11; subdirectories included via the ** glob.
plugin/lang/ is one file per language, only for languages needing
genuinely language-specific wiring: plugins, custom filetypes
(vim.filetype.add), build hooks, autocmds. Tool config (servers, formatters,
linters) lives inline in the core plugin files; per-filetype editor settings
live in ftplugin/.
vim.pack
vim.pack.add({
"https://github.com/user/repo", -- string form
{ src = "https://github.com/user/repo" }, -- table form
{ src = "https://github.com/user/repo", name = "repo" }, -- custom name
{ src = "https://github.com/user/repo", version = "main" }, -- branch/tag/commit
{ src = "https://github.com/user/repo", version = vim.version.range("1.*") }, -- semver range
})
vim.pack.update() -- interactive update with confirmation buffer
vim.pack.update({"name"}, { force = true }) -- update specific plugin, skip confirm
vim.pack.del({"name"}) -- remove from disk
vim.pack.get() -- list all managed plugins
Install location is stdpath("data") .. "/site/pack/core/opt/<name>"; the
lockfile is $XDG_CONFIG_HOME/nvim/nvim-pack-lock.json, committed to VCS.
No URL shorthand helpers in this config. The upstream docs suggest a local gh = function(x) ... end, but since vim.pack.add() is scattered across many
plugin/ files (one per plugin), a central helper adds no value. Use full URLs.
The load option decides which of the three loading patterns a file uses — see
references/loading-patterns.md.
after/lsp/ config files
Each file returns a vim.lsp.Config table; the filename (without .lua)
becomes the server name. Placed in after/lsp/ to override base configs shipped
by packages. No setup() call needed.
-- after/lsp/gopls.lua
---@type vim.lsp.Config
return {
cmd = { "gopls" },
filetypes = { "go", "gomod", "gowork", "gosum" },
root_markers = { "go.work", "go.mod", ".git" },
settings = {
gopls = {
analyses = { unusedparams = true },
staticcheck = true,
},
},
}
Servers are enabled in plugin/lsp.lua via vim.lsp.enable(servers). To
disable one: vim.lsp.enable("gopls", false).
Adding a new language
- Add LSP server to the
servers list in plugin/lsp.lua
- Add mason tools to the
ensure_installed list in plugin/mason.lua
- Add formatters to
formatters_by_ft in plugin/conform.lua
- Add linters to
linters_by_ft in plugin/lint.lua
- Testing/debugging/coverage/running:
plugin/neotest.lua, plugin/dap.lua,
plugin/nvim_coverage.lua, plugin/code_runner.lua
- (if needed)
ftplugin/<ft>.lua -- editor settings (vim.opt_local),
unless Neovim's built-in ftplugin already covers them
- (if needed)
plugin/lang/<ft>.lua -- language-specific plugins, custom
filetypes, autocmds
- (optional)
after/lsp/<server>.lua -- override nvim-lspconfig base config
1---2name: nvim-config3description: Native Neovim config idioms and conventions — use whenever writing, reviewing, or modifying any Neovim configuration that uses Neovim's built-in conventions WITHOUT a plugin manager framework (no lazy.nvim, packer, etc.). Covers directory structure, vim.pack plugin management, lsp/ auto-discovery, plugin/ loading order, keymaps, and standard paths. Trigger on any task involving init.lua, plugin/*.lua, lsp/*.lua, vim.pack.add(), vim.lsp.enable(), or "native neovim config" — even if the user just says "add a plugin" or "configure LSP" in a native-style config.4---56# Native Neovim config78Conventions for Neovim configs built on `vim.pack`, `lsp/` and `plugin/` with no9plugin manager framework. Requires Neovim >= v0.12.0.1011References — read the one the task needs:1213| File | Covers |14| -------------------------------- | ---------------------------------------------------------------------------------------------- |15| `references/loading-patterns.md` | `vim.pack.add`'s `load` option, the three loading patterns, build hooks, profiling |16| `references/plugin-files.md` | File skeleton per pattern, `_G.Config` sharing, `do/end` blocks, ftplugin, option interfaces |17| `references/startup.md` | `:h initialization` step table, runtime directories, `after/`, exrc, help tags, standard paths |1819## This config's location2021The native config lives at **`~/.dotfiles/nvim-fredrik/`** inside the dotfiles22repo. It is symlinked into place via GNU Stow:2324```25~/.dotfiles/nvim-fredrik/ <- actual files (edit here)26~/.dotfiles/stow/shared/.config/nvim-fredrik -> ../../../nvim-fredrik (stow entry)27~/.config/nvim-fredrik -> ~/.dotfiles/stow/shared/.config/nvim-fredrik (stow result)28```2930Launch it with `NVIM_APPNAME=nvim-fredrik nvim`. Apply stow symlinks after31changes from `~/.dotfiles/stow` with `packages=(shared "$(uname -s)");32host="$(hostname -s)"; [ -d "$host" ] && packages+=("$host"); stow33--target="$HOME" --restow --no-folding --adopt "${packages[@]}"`. Neovim itself34comes from nixpkgs-unstable via home-manager (`nix/shared/home/common.nix`) --35binary at `~/.nix-profile/bin/nvim`.3637## Architecture3839No framework -- each directory has a single responsibility:4041| Layer | Directory | Role |42| ------------------- | ----------------- | ---------------------------------------------------------------------------------------- |43| **options** | `lua/options.lua` | All `vim.opt` settings, required from `init.lua` |44| **utility** | `lua/` | Shared Lua modules: `lazyload.lua`, `merge.lua`, `fold.lua`, `toggle.lua`, pickers, etc. |45| **plugins** | `plugin/` | Self-contained plugin files: install + setup + keymaps |46| **lang plugins** | `plugin/lang/` | Per-language plugin installs, custom filetypes, autocmds, and setup |47| **editor settings** | `ftplugin/` | Per-filetype `vim.opt_local` (indent, wrap, conceal) |48| **server config** | `after/lsp/` | All LSP server config tables (in after/ to override package defaults) |4950```51~/.config/nvim-fredrik/52 init.lua -- leader keys, require("options"), diagnostics, keymaps53 lua/54 lazyload.lua -- VimEnter/UIEnter deferred setup queues55 merge.lua -- deep merge helper (appends+deduplicates lists, recurses dicts)56 options.lua -- all vim.opt settings57 dev.lua -- local dev plugin loader58 ... -- other utility modules (fold, toggle, pickers, icons, etc.)59 lsp/ -- (unused; nvim-lspconfig provides base configs)60 parser/ -- treesitter parser .so files (managed by nvim-treesitter)61 colors/ -- custom colorschemes (loaded by :colorscheme)62 snippets/ -- custom snippet files (loaded by blink.cmp)63 ftplugin/ -- per-filetype editor settings (vim.opt_local)64 plugin/65 lang/ -- per-language plugins, custom filetypes, autocmds66 blink.lua -- completion (VimEnter)67 conform.lua -- formatting (VimEnter)68 dap.lua -- debugging (deferred to first use)69 lint.lua -- linting (VimEnter)70 lsp.lua -- LSP enable + LspAttach keymaps (VimEnter)71 lualine.lua -- statusline (VimEnter, sync)72 mason.lua -- tool installation (VimEnter)73 neotest.lua -- testing (deferred to first use)74 <name>.lua -- other feature plugins (snacks, treesitter, oil, etc.)75 after/76 lsp/ -- all LSP server configs (overrides package defaults)77 queries/<lang>/ -- treesitter query extensions (injections.scm, etc.)78 syntax/<ft>.vim -- legacy syntax overrides/extensions79```8081Notes on the layers:8283- **`lua/lazyload.lua`** provides `on_vim_enter(fn, opts?)` and `on_ui_enter(fn,84 opts?)` for queuing setup functions. Default is async (via `vim.schedule()`);85 `{ sync = true }` runs synchronously. Also provides `on_override(fn)` for86 project-local overrides (runs after all VimEnter callbacks). Only lualine uses87 `{ sync = true }`.88- **`lua/merge.lua`** deep-merges: appends and deduplicates lists, recurses into89 dicts, overwrites scalars. `vim.NIL` as a value removes a key.90- **`lua/dev.lua`** loads a plugin from a local clone if it exists, otherwise91 falls back to `vim.pack.add()`.92- **`plugin/`** files are self-contained: `vim.pack.add()` -> setup -> keymaps.93 Sourced alphabetically at step 11; subdirectories included via the `**` glob.94- **`plugin/lang/`** is one file per language, only for languages needing95 genuinely language-specific wiring: plugins, custom filetypes96 (`vim.filetype.add`), build hooks, autocmds. Tool config (servers, formatters,97 linters) lives inline in the core plugin files; per-filetype editor settings98 live in `ftplugin/`.99100## vim.pack101102```lua103vim.pack.add({104 "https://github.com/user/repo", -- string form105 { src = "https://github.com/user/repo" }, -- table form106 { src = "https://github.com/user/repo", name = "repo" }, -- custom name107 { src = "https://github.com/user/repo", version = "main" }, -- branch/tag/commit108 { src = "https://github.com/user/repo", version = vim.version.range("1.*") }, -- semver range109})110111vim.pack.update() -- interactive update with confirmation buffer112vim.pack.update({"name"}, { force = true }) -- update specific plugin, skip confirm113vim.pack.del({"name"}) -- remove from disk114vim.pack.get() -- list all managed plugins115```116117Install location is `stdpath("data") .. "/site/pack/core/opt/<name>"`; the118lockfile is `$XDG_CONFIG_HOME/nvim/nvim-pack-lock.json`, committed to VCS.119120**No URL shorthand helpers** in this config. The upstream docs suggest a `local121gh = function(x) ... end`, but since `vim.pack.add()` is scattered across many122`plugin/` files (one per plugin), a central helper adds no value. Use full URLs.123124The `load` option decides which of the three loading patterns a file uses — see125`references/loading-patterns.md`.126127## after/lsp/ config files128129Each file returns a `vim.lsp.Config` table; the filename (without `.lua`)130becomes the server name. Placed in `after/lsp/` to override base configs shipped131by packages. No `setup()` call needed.132133```lua134-- after/lsp/gopls.lua135---@type vim.lsp.Config136return {137 cmd = { "gopls" },138 filetypes = { "go", "gomod", "gowork", "gosum" },139 root_markers = { "go.work", "go.mod", ".git" },140 settings = {141 gopls = {142 analyses = { unusedparams = true },143 staticcheck = true,144 },145 },146}147```148149Servers are enabled in `plugin/lsp.lua` via `vim.lsp.enable(servers)`. To150disable one: `vim.lsp.enable("gopls", false)`.151152## Adding a new language1531541. Add LSP server to the `servers` list in `plugin/lsp.lua`1552. Add mason tools to the `ensure_installed` list in `plugin/mason.lua`1563. Add formatters to `formatters_by_ft` in `plugin/conform.lua`1574. Add linters to `linters_by_ft` in `plugin/lint.lua`1585. Testing/debugging/coverage/running: `plugin/neotest.lua`, `plugin/dap.lua`,159 `plugin/nvim_coverage.lua`, `plugin/code_runner.lua`1606. _(if needed)_ `ftplugin/<ft>.lua` -- editor settings (`vim.opt_local`),161 unless Neovim's built-in ftplugin already covers them1627. _(if needed)_ `plugin/lang/<ft>.lua` -- language-specific plugins, custom163 filetypes, autocmds1648. _(optional)_ `after/lsp/<server>.lua` -- override nvim-lspconfig base config