NixOS: Configuring Neovim using Lazy plugin manager
This is NOT a config that you can copy+paste. Keep reading if you want to understand.
I really hate it when people publish their configs without any explanation of the years of logic behind it. This, rather, is a reflection on how I got there.
Now - why Lazy. Reason is, win10
/win11
are still my main machines (and a couple of Ubuntu
s, but those will be gone soon, NixOS is king!), and I want to keep my Neovim
config 100% the same. So, Lazy
plugin manager is my best friend. But NixOS
makes it really hard to keep the flexibility.
My journey started when I saw this idea somewhere on Reddit: you can keep using Lazy if you don’t allow it to download stuff, just lazy-load the plugins it finds. And you declare all your plugins (and all the stuff Mason is in charge of) in your config.
So at first it looked easy:
programs.neovim = {
enable = true;
withNodeJs = true;
defaultEditor = true;
viAlias = true;
package = pkgs.neovim-unwrapped; # Use pkgs from HM args, which includes overlay
# === Plugin List ===
plugins = with pkgs.vimPlugins; [
catppuccin-nvim
comment-nvim
conform-nvim
flash-nvim
... and so on ...
];
# === Extra Packages ===
extraPackages = with pkgs; [
bash-language-server
beautysh
biome
... and so on ...
];
extraLuaConfig = ''
vim.g.mapleader = " "
require("lazy").setup({
spec = {
{ import = "plugins" },
},
performance = {
reset_packpath = false,
rtp = { reset = false },
},
dev = {
path = "${pkgs.vimUtils.packDir config.programs.neovim.finalPackage.passthru.packpathDirs}/pack/myNeovimPackages/start",
patterns = { "" },
},
install = { missing = false },
})
-- Load main user config
require("config")
'';
}; # End programs.neovim
And it worked, kinda. But there's a problem. When I started using plugins that utilize TreeSitter, I discovered that, despite me loading all grammars, TreeSitter can't find any of them. The reason? Paths. So, I added:
extraLuaConfig = ''
vim.g.mapleader = " "
vim.opt.runtimepath:append("${pkgs.neovim-unwrapped}/lib/nvim/parser")
require("lazy").setup({
And everything was fine for a while, until I decided that I want a plugin that has not been packaged for Nix yet. Or the version, even in unstable, is too old. Easy enough, I thought! I'll just use vimUtils.buildVimPlugin
!
But there is a problem: these plugins are stored in their own directories, and Lazy
has no idea where they are. The solution? Overlays. I overlay vimPlugins so they contain all my self-packaged plugins, so Lazy can easily find it.
Flake.nix:
# Create package sets for each system, applying necessary overlays
pkgsFor = system: import inputs.nixpkgs {
inherit system;
overlays = [
import ./overlays/overlays.nix;
];
config = {
allowUnfree = true;
};
};
Overlays:
# ./overlays.nix
# This file aggregates all overlays for the system.
# It takes 'final' and 'prev' package sets as arguments.
# If any overlay needs access to flake inputs (like proxmox-nixos),
# change the signature to: { inputs, ... }: final: prev:
# and update the import in flake.nix accordingly.
final: prev:
let
# Import the Neovim plugin overlay function
myVimPluginsOverlayFn = import ./nvim-overlays.nix {};
# Apply the Neovim overlay to get the modified package set
# This ensures subsequent overlays can potentially see the vimPlugins changes if needed.
vimOverlayApplied = myVimPluginsOverlayFn final prev;
in
# Merge the result of the Vim overlay with other overlays defined here
vimOverlayApplied // {
# === warpd overlay (moved from flake.nix) ===
warpd = (prev.warpd.overrideAttrs (old: {
src = prev.fetchFromGitHub {
owner = "rvaiya";
repo = "warpd";
rev = "01650eabf70846deed057a77ada3c0bbb6d97d6e";
sha256 = "sha256-61+kJvOi4oog0+tGucc1rWemdx2vp15wlluJE+1PzTs=";
};
})).override { withX = false; };
}
# nvim-overlays.nix
{}:
final: prev:
{
vimPlugins = prev.vimPlugins // {
gp-nvim = prev.vimUtils.buildVimPlugin {
pname = "gp.nvim";
version = "b32327fe4ee65d24acbab0f645747c113eb935c0"; # Added version based on rev
src = prev.fetchFromGitHub {
owner = "Robitx";
repo = "gp.nvim";
rev = "b32327fe4ee65d24acbab0f645747c113eb935c0";
sha256 = "sha256-obYQyy1aHQXdf23NRNe8EPleP8KtKSisijgAQ7Ckkzo=";
};
};
blink-cmp-env = prev.vimUtils.buildVimPlugin {
pname = "blink-cmp-env";
version = "700b6d9542b9d5deea80121abbb46b1400ace302"; # Added version based on rev
src = prev.fetchFromGitHub {
owner = "bydlw98";
repo = "blink-cmp-env";
rev = "700b6d9542b9d5deea80121abbb46b1400ace302";
sha256 = "sha256-inLSnrm32k1G/lCUr+VVZBKaY9DZ7PB65BHAQ5Qpti8=";
};
};
# And so on ...
}; # End vimPlugins merge
} # End overlay function returned set
Finally, I mentioned using Neovim on multiple systems. So outside of NixOS
I use .dotfiles
. At first, just held a copy of my lua
directory in NixOS
config. But finally I added my .dotfiles
as a flake input. Note that it's a private repo, hence git+ssh
.
Flake.nix:
myDotfiles = {
url = "git+ssh://git@github.com/bogorad/.dotfiles.git?ref=main";
flake = false;
};
Imported here. Now it becomes a symlink ~/.config/nvim/lua
=> somewhere in the Nix store.
Home.nix:
# === XDG Config Files ===
xdg.configFile = {
"nvim/lua" = {
recursive = true;
source = inputs.myDotfiles + "/nvim/.config/nvim/lua";
};
}; # End xdg.configFile
Once I was at it, I also set env.vars for all programs that can be set up that way:
Home.nix:
sessionVariables = lib.mkForce {
BAT_CONFIG_DIR = "${inputs.myDotfiles}/bat/.config/bat";
JUST_JUSTFILE = "${inputs.myDotfiles}/just/.justfile";
LG_CONFIG_FILE = "${inputs.myDotfiles}/lazygit/.config/lazygit/config.yml";
MPV_HOME = "${inputs.myDotfiles}/mpv";
RIPGREP_CONFIG_PATH = "${inputs.myDotfiles}/ripgrep/.config/ripgrep/config";
STARSHIP_CONFIG = "${inputs.myDotfiles}/starship/.config/starship.toml";
WEZTERM_CONFIG_FILE = "${inputs.myDotfiles}/wezterm/.wezterm.lua";
YAZI_CONFIG_HOME = "${inputs.myDotfiles}/yazi/.config/yazi";
};
And this is it :)