nvf — minimal Neovim config
Neovim configured with nvf.
No runtime plugin manager — everything is pinned and built by Nix.
Language tooling (LSP servers, treesitter parsers) is delegated entirely
to per-project devshells so the editor package stays small.
Structure
flake.nix inputs, package output, config devShell
nvim.nix root module — just a list of imports
modules/
options.nix vim.o equivalents, clipboard, leader keys
ui.nix theme (catppuccin frappe), mini.statusline,
gitsigns, autopairs, comments
treesitter.nix meta-language grammars only (nix, lua, bash,
markdown, gitcommit, git_rebase)
treesitter-textobjects.nix query files for mini-ai text objects
lsp.nix LSP infra — no servers bundled
lazydev.nix Neovim API type stubs for lua_ls
completion.nix blink.cmp with Rust fuzzy engine
picker.nix mini.pick + mini.extra + mini.visits + mini.icons
whichkey.nix which-key keybinding hints + group labels
oil.nix oil.nvim filesystem editor
plenary.nix plenary.nvim shared dependency
neogit.nix neogit git interface
diffview.nix diffview.nvim diff/history viewer
fidget.nix LSP progress spinner (bottom-right)
flash.nix flash.nvim jump motions
surround.nix mini.surround text object wrappers
conform.nix conform.nvim autoformat (devshell-driven)
lint.nix nvim-lint async linting (devshell-driven)
mini-ai.nix mini.ai extended text objects (treesitter-powered)
mini-sessions.nix mini.sessions: auto-restore on startup, direnv + LSP reload on switch
mini-notify.nix mini.notify floating notification UI
todo-comments.nix todo-comments.nvim highlight + navigation
grug-far.nix grug-far.nvim live find-and-replace
vim-sleuth.nix auto-detect indentation per file
keymaps.nix keymaps, autocmds, LspAttach, parser path hook
Keybindings
<Space> is the leader key. Press it and wait to see which-key hints.
Navigation
| Key |
Action |
<C-h/j/k/l> |
Move between windows |
<C-↑/↓/←/→> |
Resize windows |
<S-h> / <S-l> |
Previous / next buffer |
- |
Open parent directory in oil |
jk |
Exit insert mode |
Find (<leader>f)
| Key |
Action |
<leader>ff |
Find files |
<leader>fr |
Frecency files (recent + frequent) |
<leader>fg |
Live grep |
<leader>fb |
Open buffers |
<leader>fh |
Help tags |
<leader>fd |
Diagnostics picker |
<leader>fs |
Document symbols |
<leader>fS |
Workspace symbols |
<leader>ft |
Todo list |
<leader>fR |
Find & replace (grug-far) |
Git (<leader>g)
| Key |
Action |
<leader>gg |
Neogit status |
<leader>gC |
Neogit commit popup |
<leader>gc |
Git commits (picker) |
<leader>gb |
Git branches (picker) |
<leader>gd |
Diffview open (working tree vs index) |
<leader>gh |
File history (current file) |
<leader>gH |
Repo history |
Inside neogit: s stage, u unstage, c commit, P push, F pull, d open diffview, q quit.
Inside diffview: <Tab>/<S-Tab> cycle files, [c/]c hunks, q close, g? all bindings.
Surround (ys, cs, ds)
| Key |
Action |
ys{motion}{char} |
Add surrounding e.g. ysiw" → "word" |
yss{char} |
Surround whole line |
cs{old}{new} |
Change surrounding e.g. cs"' → 'word' |
ds{char} |
Delete surrounding e.g. ds" → word |
S{char} |
Surround visual selection |
Text objects (mini.ai)
Works with operators (d, c, y, v) and dot-repeat. a = around, i = inside.
| Object |
Target |
af / if |
Function (treesitter) |
ac / ic |
Class (treesitter) |
aa / ia |
Argument / parameter (treesitter) |
ab / ib |
Any bracket pair ( [ { |
aq / iq |
Any quote " ' `` |
Flash (jump)
| Key |
Action |
s{char}{char} |
Jump to any visible position |
S{char}{char} |
Treesitter-aware node jump |
r (op-pending) |
Remote flash — apply operator at distance |
Code / LSP (<leader>c, gr*)
| Key |
Action |
<leader>cf |
Format buffer |
<leader>cd |
Show diagnostic float |
[d / ]d |
Previous / next diagnostic |
gd |
Go to definition |
gD |
Go to declaration |
K |
Hover docs |
grr |
References (0.12 built-in) |
gri |
Implementation (0.12 built-in) |
grn |
Rename (0.12 built-in) |
gra |
Code action (0.12 built-in) |
gO |
Document symbols (0.12 built-in) |
UI toggles (<leader>u)
| Key |
Action |
<leader>un |
Notification history |
<leader>uf |
Toggle autoformat (buffer-local) |
<leader>ud |
Toggle diagnostics |
Buffers (<leader>b)
| Key |
Action |
<leader>bd |
Delete buffer |
Sessions (<leader>s)
On startup with no file arguments, the session for the current directory is restored
automatically. Sessions are named by full path (/home/user/proj → %home%user%proj)
so projects with the same basename don't collide.
| Key |
Action |
<leader>ss |
Save session for current directory |
<leader>sl |
Browse sessions by recency and load |
<leader>sd |
Delete a saved session |
Switching sessions stops the old project's LSP servers, reloads the new project's
direnv environment, and restarts LSP/formatters/linters from the updated env vars.
| Key |
Action |
]t / [t |
Next / previous todo |
Misc
| Key |
Action |
<leader>: |
Command history buffer (cmdwin) |
<leader>U |
Undotree (0.12 built-in) |
<leader>q |
Diagnostics quickfix list |
<leader>Q |
Quit all |
Oil (<leader>e, -)
| Key |
Action |
- |
Open parent directory (full window) |
<leader>e |
Open oil (floating window) |
<CR> |
Open file or directory |
g. |
Toggle hidden files |
:w |
Apply changes (rename, delete, move) |
Local usage
nix run .# # open Neovim
nix build .# # build into ./result/bin/nvim
nix develop # enter the config devShell (lua-language-server)
nix flake update # bump nvf + nixpkgs, then nix build .#
Using this repo in another flake
Once this config is in a public git repository, other flakes can
consume it directly as an input. The built Neovim package is exposed
as packages.<system>.default.
NixOS (system-wide)
# your system flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
my-nvim.url = "github:yourusername/your-nvim-repo";
};
outputs = { nixpkgs, my-nvim, ... }: {
nixosConfigurations.hostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
({ pkgs, ... }: {
environment.systemPackages = [
my-nvim.packages.x86_64-linux.default
];
})
];
};
};
}
Home Manager (per-user)
# your home.nix or home-manager module
{ inputs, pkgs, ... }: {
home.packages = [
inputs.my-nvim.packages.${pkgs.stdenv.hostPlatform.system}.default
];
# optional: make it the default editor
home.sessionVariables.EDITOR = "nvim";
}
Using pkgs.stdenv.hostPlatform.system instead of a hardcoded string
means the same config works on x86_64-linux, aarch64-linux,
x86_64-darwin, and aarch64-darwin without changes.
Updating independently
# update only this nvim config, leave everything else pinned
nix flake update my-nvim
# update everything
nix flake update
Roll back to a specific commit:
nix flake lock --update-input my-nvim \
--override-input my-nvim github:yourusername/your-nvim-repo/<commit-sha>
Integrating nvf as a NixOS / Home Manager module
If you prefer nvf's module system over a pre-built package:
# NixOS
imports = [ inputs.nvf.nixosModules.default ];
programs.nvf.enable = true;
programs.nvf.settings = (import ./nvim.nix { inherit pkgs; }).config.vim;
# Home Manager
imports = [ inputs.nvf.homeManagerModules.default ];
programs.nvf.enable = true;
programs.nvf.settings = (import ./nvim.nix { inherit pkgs; }).config.vim;
Project devShell template
Copy this into your project's flake.nix and fill in sections 1–3.
The Neovim package itself is not listed here — it is expected to be in
your system packages or on $PATH already.
How it works
- LSP servers are plain executables.
lspconfig finds them by name on
$PATH; adding them to packages is all that is needed.
- Treesitter parsers are built by Nix. The
shellHook sets
NVIM_TREESITTER_PARSER_PATH, which modules/keymaps.nix reads at
startup and appends to runtimepath. Outside a devshell the variable
is unset and the block is a silent no-op.
# In your project's flake.nix outputs:
devShells = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
# ── 1. Treesitter parsers ──────────────────────────────────────
# Use withPlugins + symlinkJoin of .dependencies — NOT builtGrammars
# directly. nixpkgs has two levels of grammar derivations:
#
# builtGrammars.{lang} — raw grammar; `parser` is a plain file
# withPlugins.dependencies — processed via grammarToPlugin/
# toNvimTreesitterGrammar; each has
# `parser/{lang}.so` — the layout Neovim
# expects when searching runtimepath.
#
# symlinkJoin merges all `parser/{lang}.so` files into one root with
# no name collisions. The result also includes per-language query files.
#
# Browse available parsers in a Nix repl:
# nix repl> :l <nixpkgs>
# nix repl> pkgs.vimPlugins.nvim-treesitter.builtGrammars.<TAB>
projectParsers =
let
withSelectedParsers = pkgs.vimPlugins.nvim-treesitter.withPlugins (p: [
p.python
p.typescript
p.javascript
p.tsx
p.rust
p.go
p.html
p.css
p.json
p.yaml
p.toml
]);
in
pkgs.symlinkJoin {
name = "project-nvim-parsers";
paths = withSelectedParsers.dependencies;
};
in {
default = pkgs.mkShell {
packages = [
# ── 2. LSP servers ──────────────────────────────────────────
# Server ↔ nixpkgs package mapping:
# https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md
# Search: nix search nixpkgs <server-name>
# Python
pkgs.pyright
# pkgs.ruff # linter + formatter
# TypeScript / JavaScript
pkgs.nodePackages.typescript-language-server
# pkgs.nodePackages.prettier # formatter
# Rust
# pkgs.rust-analyzer
# Go
# pkgs.gopls
# Lua
# pkgs.lua-language-server
# Nix — nixd LSP, alejandra formatter, statix + deadnix linters are
# bundled in the Neovim package; no need to add them here.
# ── 3. Other project tools ──────────────────────────────────
# pkgs.nodejs
# pkgs.cargo
];
shellHook = ''
# Expose Nix-built parsers to Neovim.
# projectParsers is a derivation with parser/{lang}.so files.
# modules/keymaps.nix appends this path to runtimepath at startup;
# modules/treesitter.nix handles the nvim-treesitter runtime/ fix
# and the registry patch for text-object / indent support.
export NVIM_TREESITTER_PARSER_PATH="${projectParsers}"
# ── 4. Formatters and linters ──────────────────────────────
# JSON objects mapping filetypes to tool lists.
# Tools must be in packages above to be on $PATH.
# modules/conform.nix and modules/lint.nix read these at startup
# and merge them with the hardcoded nix/alejandra + nix/statix entries.
#
# Formatter names: https://github.com/stevearc/conform.nvim#formatters
# Linter names: https://github.com/mfussenegger/nvim-lint#available-linters
export NVIM_FORMATTERS='{"python":["black"],"javascript":["prettier"]}'
export NVIM_LINTERS='{"python":["ruff"],"javascript":["eslint_d"]}'
# JSON array of nvim-lspconfig server names to enable.
# Servers must be on $PATH (installed above or via the project's package manager).
# Config names: https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md
export NVIM_LSP_SERVERS='["pyright","ruff"]'
'';
};
});