Files
nixos-config/modules/nvim.nix
T
daniil-bergandClaude Opus 5 93a4a934eb feat(nvim): accept completion with <CR>, indent 4
blink.cmp uses the `enter` preset with `completion.list.selection.preselect =
false`, so an item is taken only once selected with `<Down>`/`<C-n>` and `<CR>`
otherwise inserts a newline. `<C-y>` stays bound as vim's own confirm key.

`<Esc>` and `<C-e>` both run `cancel` rather than the preset's `hide`: hiding
leaves the auto_insert preview in the buffer without the `additionalTextEdits`
and snippet expansion a real accept applies, and builtin ins-completion `<C-e>`
likewise restores the typed text.

`shiftwidth`/`tabstop` are 4, matching PHP's PSR-12. PHP's `indentkeys` carry
`*<Return>`, which reindents the just-finished line on every newline; at width 2
that silently re-indented 4-space files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015D4NsszUG7hy9ghKZ4nfhm
2026-09-15 12:47:21 +02:00

303 lines
14 KiB
Nix

# Neovim, configured declaratively via nixvim. The editor — plugins, LSP,
# completion, keymaps — is defined here and pinned by the flake lock; nothing is
# downloaded at runtime, unlike an imperative distro (NvChad/LazyVim). The nixvim
# home-manager module that provides `programs.nixvim` is imported for consumers by
# `homeModules.default` in flake.nix, so this file only sets options.
#
# Consumer knobs (the option-with-default contract): `local.nvim.enable` to opt
# out wholesale, `local.nvim.colorscheme` to swap theme in one line, and
# `local.nvim.lspServers` to reshape the language set. Everything else is
# opinionated — but since nixvim exposes every plugin as an option, a consumer can
# still override any `programs.nixvim.plugins.*` directly from their own config.
{ config, lib, pkgs, ... }:
let
cfg = config.local.nvim;
in
{
options.local.nvim = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable the nixvim-based Neovim setup.";
};
colorscheme = lib.mkOption {
type = lib.types.str;
default = "catppuccin";
example = "tokyonight";
description = ''
nixvim colorscheme module to enable (the attr under `colorschemes.*`,
e.g. "catppuccin", "tokyonight", "gruvbox"). Catppuccin is tuned below
(mocha flavour); for another scheme set its `colorschemes.<name>.settings`
from your own config if you want to tweak it.
'';
};
lspServers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"nixd" # Nix — this repo's own language, so a sensible public baseline
"bashls" # Shell — likewise generic enough to default on
];
example = [ "nixd" "bashls" "rust_analyzer" "basedpyright" ];
description = ''
lspconfig server names to enable under `plugins.lsp.servers.<name>`.
Each must exist in nixvim's server list and its binary in nixpkgs. The
public default is deliberately minimal (Nix + shell); a consumer sets its
own language set here — and any per-server tuning a pick needs (e.g.
rust_analyzer's `installRustc`) via `programs.nixvim.plugins.lsp.servers`.
Note: standalone Lua wants "lua_ls", but Lua embedded in Nix strings is
invisible to any LSP, so it is not needed just to edit this config.
'';
};
};
config = lib.mkIf cfg.enable {
programs.nixvim = {
enable = true;
# Point nixvim at our (followed) nixpkgs instead of the rev it pins. Both
# track nixos-26.05, so the version skew nixvim warns about does not arise,
# and this keeps a single nixpkgs in the closure. Setting it explicitly is
# also what silences that warning.
nixpkgs.source = pkgs.path;
# System clipboard on Wayland: yanks go to / puts read from wl-clipboard,
# so copy/paste crosses the nvim boundary. Enabling the provider pulls the
# wl-clipboard binaries in as a dependency.
clipboard = {
register = "unnamedplus";
providers.wl-copy.enable = true;
};
globals = {
mapleader = " ";
maplocalleader = " ";
};
opts = {
number = true;
relativenumber = true; # relative line numbers for quick j/k motions
signcolumn = "yes"; # always show the gutter so text doesn't jump
cursorline = true;
termguicolors = true; # 24-bit colour, required by catppuccin & co.
scrolloff = 8; # keep 8 lines of context around the cursor
expandtab = true; # spaces, not tabs
shiftwidth = 4;
tabstop = 4;
smartindent = true;
ignorecase = true; # case-insensitive search…
smartcase = true; # …unless the query has a capital
undofile = true; # persistent undo across sessions
splitright = true;
splitbelow = true;
# What :mksession stores (persistence.nvim). Drops "blank" and "terminal"
# from nvim's default so empty/plugin windows (neo-tree, toggleterm) aren't
# serialised — they restore as broken empty splits otherwise. Adds
# "globals", which some plugins rely on. Pairs with the PersistenceSavePre
# neo-tree close below.
sessionoptions = "buffers,curdir,folds,globals,tabpages,winsize";
};
# Theme. Swap via local.nvim.colorscheme; catppuccin gets the mocha flavour.
# mkMerge keeps the computed `${cfg.colorscheme}` key from colliding with the
# literal `catppuccin` one when they happen to be the same attr.
colorschemes = lib.mkMerge [
{ ${cfg.colorscheme}.enable = true; }
(lib.mkIf (cfg.colorscheme == "catppuccin") {
catppuccin.settings.flavour = "mocha";
})
];
plugins = {
# Shared icon set for the tree, statusline, completion menu.
web-devicons.enable = true;
# Syntax via tree-sitter. The default grammar set is broad, so most
# languages get highlighting without an LSP or any per-language wiring.
treesitter = {
enable = true;
settings = {
highlight.enable = true;
indent.enable = true;
};
};
# LSP. Servers come from local.nvim.lspServers; blink-cmp feeds its
# completion capabilities in automatically (setupLspCapabilities).
lsp = {
enable = true;
servers = lib.genAttrs cfg.lspServers (_: { enable = true; });
keymaps = {
silent = true;
lspBuf = {
gd = "definition";
gD = "declaration";
K = "hover";
"<leader>rn" = "rename";
"<leader>ca" = "code_action";
};
# gr/gi (references, implementations) are wired to Telescope pickers in
# the global keymaps below instead — fuzzy + preview browsing beats the
# quickfix list that vim.lsp.buf.references would populate. gd stays a
# direct jump here (usually one target; Ctrl-o returns).
diagnostic = {
# Idiomatic vim bracket pair for stepping a list, kept as the public
# default. A consumer whose layout makes brackets awkward (they need
# AltGr on QWERTZ) can add comfortable aliases from their own config:
# this option is attrsOf, so extra keys merge in without conflict.
"[d" = "goto_prev";
"]d" = "goto_next";
"<leader>ds" = "open_float"; # show the diagnostic under the cursor
};
};
};
# Completion. blink.cmp — faster and lighter to configure than nvim-cmp.
blink-cmp = {
enable = true;
settings = {
# "enter" preset: <CR> accepts, Up/Down and C-n/C-p move, Tab is
# left to snippet jumping. Custom keys below merge over the preset.
keymap = {
preset = "enter";
# Vim's own ins-completion confirm key, kept alongside <CR>.
"<C-y>" = [ "select_and_accept" "fallback" ];
# Dismiss the menu and undo the auto_insert preview, staying in
# insert mode. `cancel` rather than the preset's `hide`, which
# would leave the previewed text in the buffer without the
# additionalTextEdits (imports) and snippet expansion that a real
# accept applies — a degraded accept nobody wants. This also
# matches builtin ins-completion, where C-e restores the typed
# text. With no menu open both keys fall back to their native
# meaning (Esc leaves insert mode).
"<C-e>" = [ "cancel" "fallback" ];
"<Esc>" = [ "cancel" "fallback" ];
};
appearance.nerd_font_variant = "mono";
sources.default = [ "lsp" "path" "snippets" "buffer" ];
completion = {
documentation.auto_show = true;
# No item is selected until one is picked with Down/C-n, so <CR>
# inserts a newline unless a completion was deliberately chosen.
list.selection.preselect = false;
};
};
};
# Fuzzy finder — files, live grep, open buffers (buffer switching lives
# here rather than a bufferline; see the S-h/S-l binds below too).
telescope.enable = true;
# File tree.
neo-tree.enable = true;
# Statusline, NvChad-ish blocks: powerline separators, one global bar,
# theme following the colorscheme.
lualine = {
enable = true;
settings.options = {
theme = "auto";
globalstatus = true;
section_separators = { left = ""; right = ""; };
component_separators = { left = ""; right = ""; };
};
};
# Inline colour swatches for #rrggbb / colour names.
colorizer.enable = true;
# Git signs in the gutter.
gitsigns.enable = true;
# Session persistence: auto-saves a session per project directory into XDG
# state, restorable later. <leader>S restores the current dir's session; a
# consumer wanting auto-restore on a bare `nvim` adds a VimEnter autocmd.
persistence.enable = true;
# Terminal split (shares nvim's cwd), toggled via <leader>t{h,v} below.
# No open_mapping — the leader binds cover both directions; size applies
# to whichever split is open (rows when horizontal, cols when vertical).
toggleterm = {
enable = true;
settings = {
direction = "horizontal";
size = 15;
};
};
# Popup of the pending keybinds after leader — a live, always-accurate
# cheatsheet. The group labels name each leader subtree so the popup reads
# by category; <leader>? opens the full list on demand (keymaps below).
which-key = {
enable = true;
settings.spec = [
{ __unkeyed-1 = "<leader>f"; group = "Find (Telescope)"; }
{ __unkeyed-1 = "<leader>d"; group = "Diagnostics"; }
{ __unkeyed-1 = "<leader>b"; group = "Buffer"; }
{ __unkeyed-1 = "<leader>c"; group = "Code"; }
{ __unkeyed-1 = "<leader>r"; group = "Refactor"; }
{ __unkeyed-1 = "<leader>s"; group = "Search"; }
{ __unkeyed-1 = "<leader>t"; group = "Terminal"; }
];
};
};
# Global (non-LSP) keymaps. Deliberately small — a starting point to grow.
keymaps = [
{ mode = "n"; key = "<leader>w"; action = "<cmd>w<cr>"; options.desc = "Save"; }
{ mode = "n"; key = "<leader>q"; action = "<cmd>q<cr>"; options.desc = "Quit window"; }
{ mode = "n"; key = "<leader>Q"; action = "<cmd>qa<cr>"; options.desc = "Quit all (exit nvim)"; }
# Restore the saved session for the current directory (persistence.nvim).
{ mode = "n"; key = "<leader>S"; action.__raw = "function() require('persistence').load() end"; options.desc = "Restore session"; }
{ mode = "n"; key = "<leader>e"; action = "<cmd>Neotree toggle<cr>"; options.desc = "File tree"; }
{ mode = "n"; key = "<Esc>"; action = "<cmd>nohlsearch<cr>"; }
# Telescope
{ mode = "n"; key = "<leader>ff"; action = "<cmd>Telescope find_files<cr>"; options.desc = "Find files"; }
{ mode = "n"; key = "<leader>fg"; action = "<cmd>Telescope live_grep<cr>"; options.desc = "Live grep"; }
{ mode = "n"; key = "<leader>fb"; action = "<cmd>Telescope buffers<cr>"; options.desc = "Buffers"; }
{ mode = "n"; key = "<leader>fh"; action = "<cmd>Telescope help_tags<cr>"; options.desc = "Help tags"; }
# LSP navigation via Telescope pickers (fuzzy + preview) for the
# many-result lookups; gd/gD stay direct jumps (plugins.lsp.keymaps above).
{ mode = "n"; key = "gr"; action = "<cmd>Telescope lsp_references<cr>"; options.desc = "References"; }
{ mode = "n"; key = "gi"; action = "<cmd>Telescope lsp_implementations<cr>"; options.desc = "Implementations"; }
# Buffer navigation (no bufferline: cycle with Shift-h/l, list via <leader>fb)
{ mode = "n"; key = "<S-l>"; action = "<cmd>bnext<cr>"; options.desc = "Next buffer"; }
{ mode = "n"; key = "<S-h>"; action = "<cmd>bprevious<cr>"; options.desc = "Prev buffer"; }
{ mode = "n"; key = "<leader>bd"; action = "<cmd>bdelete<cr>"; options.desc = "Close buffer"; }
# Terminal: toggle a horizontal or vertical split (toggleterm). Same key
# hides it again. From inside the terminal, <C-w> drops to normal mode and
# runs the window command, so leaving is one chord (no <C-\><C-n> dance).
{ mode = "n"; key = "<leader>th"; action = "<cmd>ToggleTerm direction=horizontal<cr>"; options.desc = "Terminal (horizontal)"; }
{ mode = "n"; key = "<leader>tv"; action = "<cmd>ToggleTerm direction=vertical size=80<cr>"; options.desc = "Terminal (vertical)"; }
{ mode = "t"; key = "<C-w>"; action = ''<C-\><C-n><C-w>''; options.desc = "Leave terminal to window"; }
# Cheatsheet: <leader>? pops the full which-key list; <leader>sk fuzzy-
# searches every active mapping via Telescope (catches non-leader binds too).
{ mode = "n"; key = "<leader>?"; action = "<cmd>WhichKey<cr>"; options.desc = "All keybinds (which-key)"; }
{ mode = "n"; key = "<leader>sk"; action = "<cmd>Telescope keymaps<cr>"; options.desc = "Search keymaps"; }
];
# persistence saves the window layout via :mksession, but neo-tree's special
# buffer doesn't serialise — a saved tree window restores as a blank split.
# Close neo-tree just before a session is written so the layout stays clean.
autoCmd = [
{
event = "User";
pattern = "PersistenceSavePre";
callback.__raw = ''function() pcall(vim.cmd, "Neotree close") end'';
}
];
};
};
}