# 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..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.`. 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 = 2; tabstop = 2; 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"; "rn" = "rename"; "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"; "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 = { keymap.preset = "default"; # C-space open, C-y confirm, C-n/C-p move appearance.nerd_font_variant = "mono"; sources.default = [ "lsp" "path" "snippets" "buffer" ]; completion.documentation.auto_show = true; }; }; # 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. S restores the current dir's session; a # consumer wanting auto-restore on a bare `nvim` adds a VimEnter autocmd. persistence.enable = true; # Floating terminal toggled with Ctrl-\ (shares nvim's cwd). Try it; if # a tiled foot next to nvim serves you better, drop this block. toggleterm = { enable = true; settings = { open_mapping = { __raw = "[[]]"; }; direction = "float"; }; }; # 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; ? opens the full list on demand (keymaps below). which-key = { enable = true; settings.spec = [ { __unkeyed-1 = "f"; group = "Find (Telescope)"; } { __unkeyed-1 = "d"; group = "Diagnostics"; } { __unkeyed-1 = "b"; group = "Buffer"; } { __unkeyed-1 = "c"; group = "Code"; } { __unkeyed-1 = "r"; group = "Refactor"; } { __unkeyed-1 = "s"; group = "Search"; } ]; }; }; # Global (non-LSP) keymaps. Deliberately small — a starting point to grow. keymaps = [ { mode = "n"; key = "w"; action = "w"; options.desc = "Save"; } { mode = "n"; key = "q"; action = "q"; options.desc = "Quit window"; } { mode = "n"; key = "Q"; action = "qa"; options.desc = "Quit all (exit nvim)"; } # Restore the saved session for the current directory (persistence.nvim). { mode = "n"; key = "S"; action.__raw = "function() require('persistence').load() end"; options.desc = "Restore session"; } { mode = "n"; key = "e"; action = "Neotree toggle"; options.desc = "File tree"; } { mode = "n"; key = ""; action = "nohlsearch"; } # Telescope { mode = "n"; key = "ff"; action = "Telescope find_files"; options.desc = "Find files"; } { mode = "n"; key = "fg"; action = "Telescope live_grep"; options.desc = "Live grep"; } { mode = "n"; key = "fb"; action = "Telescope buffers"; options.desc = "Buffers"; } { mode = "n"; key = "fh"; action = "Telescope help_tags"; 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 = "Telescope lsp_references"; options.desc = "References"; } { mode = "n"; key = "gi"; action = "Telescope lsp_implementations"; options.desc = "Implementations"; } # Buffer navigation (no bufferline: cycle with Shift-h/l, list via fb) { mode = "n"; key = ""; action = "bnext"; options.desc = "Next buffer"; } { mode = "n"; key = ""; action = "bprevious"; options.desc = "Prev buffer"; } { mode = "n"; key = "bd"; action = "bdelete"; options.desc = "Close buffer"; } # Cheatsheet: ? pops the full which-key list; sk fuzzy- # searches every active mapping via Telescope (catches non-leader binds too). { mode = "n"; key = "?"; action = "WhichKey"; options.desc = "All keybinds (which-key)"; } { mode = "n"; key = "sk"; action = "Telescope keymaps"; 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''; } ]; }; }; }