Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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
.claude/skills/fredrikaverpil-nvim-config/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 71% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 44% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 115% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 38% | 0% |
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 |
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 with cd ~/.dotfiles/stow && stow --target="$HOME" --restow --no-folding --adopt shared "$(uname -s)". Neovim itself is managed by Bob, not nixpkgs -- binary at ~/.local/share/bob/nvim-bin/nvim.
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/extensionsNotes 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 intodicts, overwrites scalars. vim.NIL as a value removes a key.
lua/dev.lua loads a plugin from a local clone if it exists, otherwisefalls 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 needinggenuinely 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/.
luavim.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.
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.
lua-- 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).
servers list in plugin/lsp.luaensure_installed list in plugin/mason.luaformatters_by_ft in plugin/conform.lualinters_by_ft in plugin/lint.luaplugin/neotest.lua, plugin/dap.lua,plugin/nvim_coverage.lua, plugin/code_runner.lua
ftplugin/<ft>.lua -- editor settings (vim.opt_local),unless Neovim's built-in ftplugin already covers them
plugin/lang/<ft>.lua -- language-specific plugins, customfiletypes, autocmds
after/lsp/<server>.lua -- override nvim-lspconfig base config| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 12,478 | 12,737 | +2% | 1 | 1 | 0% | 2,432 | 4,167 | +71% | 0 | 0 | — |
case-02 | fail→pass | 13,412 | 6,848 | -49% | 1 | 1 | 0% | 2,452 | 3,538 | +44% | 0 | 0 | — |
case-03 | fail→pass | 12,169 | 9,002 | -26% | 1 | 1 | 0% | 2,336 | 3,908 | +67% | 0 | 0 | — |
case-04 | fail→pass | 8,192 | 5,504 | -33% | 1 | 1 | 0% | 1,561 | 3,350 | +115% | 0 | 0 | — |
case-05 | pass→pass | 6,914 | 5,524 | -20% | 1 | 1 | 0% | 1,278 | 3,170 | +148% | 0 | 0 | — |
case-06 | pass→pass | 5,276 | 3,112 | -41% | 1 | 1 | 0% | 935 | 2,777 | +197% | 0 | 0 | — |
case-07 | fail→pass | 13,324 | 3,636 | -73% | 1 | 1 | 0% | 2,046 | 2,824 | +38% | 0 | 0 | — |
case-08 | fail→pass | 12,512 | 7,011 | -44% | 1 | 1 | 0% | 2,034 | 3,430 | +69% | 0 | 0 | — |
case-09 | fail→pass | 14,829 | 5,306 | -64% | 1 | 1 | 0% | 2,369 | 3,167 | +34% | 0 | 0 | — |
case-10 | pass→pass | 14,283 | 2,954 | -79% | 1 | 1 | 0% | 2,558 | 2,731 | +7% | 0 | 0 | — |
case-11 | fail→pass | 14,648 | 3,458 | -76% | 1 | 1 | 0% | 2,508 | 2,819 | +12% | 0 | 0 | — |
case-12 | pass→pass | 10,654 | 5,323 | -50% | 1 | 1 | 0% | 2,007 | 3,130 | +56% | 0 | 0 | — |
case-13 | fail→fail | 2,327 | 1,317 | -43% | 1 | 1 | 0% | 336 | 2,400 | +614% | 0 | 0 | — |
case-14 | pass→pass | 13,359 | 7,800 | -42% | 1 | 1 | 0% | 2,071 | 3,455 | +67% | 0 | 0 | — |
case-15 | fail→pass | 11,463 | 2,625 | -77% | 1 | 1 | 0% | 2,018 | 2,674 | +33% | 0 | 0 | — |
case-16 | pass→pass | 8,725 | 2,669 | -69% | 1 | 1 | 0% | 1,558 | 2,632 | +69% | 0 | 0 | — |
case-17 | pass→pass | 7,852 | 2,780 | -65% | 1 | 1 | 0% | 1,470 | 2,655 | +81% | 0 | 0 | — |
case-18 | fail→pass | 13,117 | 3,011 | -77% | 1 | 1 | 0% | 2,182 | 2,804 | +29% | 0 | 0 | — |
case-19 | fail→pass | 7,840 | 4,790 | -39% | 1 | 1 | 0% | 1,417 | 3,062 | +116% | 0 | 0 | — |
case-20 | fail→pass | 5,816 | 1,419 | -76% | 1 | 1 | 0% | 972 | 2,430 | +150% | 0 | 0 | — |
case-21 | fail→pass | 16,322 | 2,136 | -87% | 1 | 1 | 0% | 3,041 | 2,572 | -15% | 0 | 0 | — |
case-22 | fail→pass | 10,637 | 2,059 | -81% | 1 | 1 | 0% | 1,749 | 2,504 | +43% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +64 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.