Nix: Where are my neovim plugins?
I was recently trying to figure my home manager setup and wasn’t sure why a plugin wasn’t working.
The neovim part of this configuration looks something like this,
programs.neovim = {
enable = true;
plugins = with pkgs.vimPlugins; [
nvim-solarized-lua
editorconfig-vim
vim-airline
nvim-treesitter.withAllGrammars
];
extraLuaConfig = ''
vim.opt.ai = true
vim.opt.encoding = 'utf8'
vim.opt.expandtab = true
vim.opt.swapfile = false
vim.opt.number = true
vim.opt.shiftwidth = 4
'';
};
Before things broke, this worked auto-magically for me and I didn’t think about it much. The plugins probably just exist in my neovim configuration, right?
$ ls ls ~/.config/nvim
init.lua
Maybe they’re imported from the nix store from that file?
$ cat ~/.config/nvim/init.lua
vim.opt.ai = true
vim.opt.encoding = 'utf8'
vim.opt.expandtab = true
vim.opt.swapfile = false
vim.opt.number = true
vim.opt.shiftwidth = 4
No, the file in this case is exactly the contents of extraLuaConfig
.
There’s no sign of any of the plugins, even the ones that are definitely loaded.
I dig closer I looked at the home-manager source.
The the module for neovim lives in modules/programs/neovim.nix
.
Whenever you’re reading a home manager module there’s two main parts you want to look at.
First, options
which defines the type of the configuration and has places to fill in documentation.
Here’s the docs for vimPlugins
,
plugins = mkOption {
type = with types; listOf (either package pluginWithConfigType);
default = [ ];
example = literalExpression ''
with pkgs.vimPlugins; [
yankring
vim-nix
{ plugin = vim-startify;
config = "let g:startify_change_to_vcs_root = 0";
}
]
'';
description = ''
List of vim plugins to install optionally associated with
configuration to be placed in init.vim.
This option is mutually exclusive with {var}`configure`.
'';
};
This tells us we should provide a list of plugins, or plugins with configurtion attached, and gives an example. But I’m trying to figure out how modules are loaded, and this isn’t helping me.
The other part is config
which defines the changes that get merged into the configuration.
We can see where init.lua
gets generated.
xdg.configFile =
let hasLuaConfig = hasAttr "lua" config.programs.neovim.generatedConfigs;
in mkMerge (
# writes runtime
(map (x: x.runtime) pluginsNormalized) ++ [{
"nvim/init.lua" = let
luaRcContent =
lib.optionalString (neovimConfig.neovimRcContent != "")
"vim.cmd [[source ${
pkgs.writeText "nvim-init-home-manager.vim"
neovimConfig.neovimRcContent
}]]" + config.programs.neovim.extraLuaConfig
+ lib.optionalString hasLuaConfig
config.programs.neovim.generatedConfigs.lua;
in mkIf (luaRcContent != "") { text = luaRcContent; };
"nvim/coc-settings.json" = mkIf cfg.coc.enable {
source = jsonFormat.generate "coc-settings.json" cfg.coc.settings;
};
}]);
I find nix is often written in a way that’s difficult to read, but it’s easier if we remove the parts that aren’t relevant to us.
The (map (x: x.runtime) pluginsNormalized)
part is only used when we pass plugins with a runtime
option. Since I’m not doing this, this can be ignored.
The next part says create a file in the XDG config directory (usually ~/.config
) called nvim/init.lua
.
This file is set to a string composed of three parts concatenated together.
The first relies on neovimRcContent
which we’re not using, so it’s empty.
The next is the extraLuaConfig
I defined in my config.
The last part is also part of something I’m not using.
So, the end result just has the extraLuaContent
in it.
This explains why how init.lua
is generated but still leaves how plugins are loaded a mystery.
The plugins are used in another place to generate a variable neovimConfig
, which is part of the generation of finalPackage
.
neovimConfig = pkgs.neovimUtils.makeNeovimConfig {
inherit (cfg) extraPython3Packages withPython3 withRuby viAlias vimAlias;
withNodeJs = cfg.withNodeJs || cfg.coc.enable;
plugins = map suppressNotVimlConfig pluginsNormalized;
customRC = cfg.extraConfig;
};
# ...
programs.neovim.finalPackage = pkgs.wrapNeovimUnstable cfg.package
(neovimConfig // {
wrapperArgs = (lib.escapeShellArgs neovimConfig.wrapperArgs) + " "
+ extraMakeWrapperArgs + " " + extraMakeWrapperLuaCArgs + " "
+ extraMakeWrapperLuaArgs;
wrapRc = false;
});
A reasonable assumption is finalPackage
is the derivation that includes the neovim executable.
Instead of going deeper through the code, I’ll check this assumption by looking at the content of that derivation.
Are there files related to editorconfig
in there?
$ realpath ~/.nix-profile/bin/nvim
/nix/store/1haqzsv4n0v0jdpdc3ab3a2if6i79sk9-neovim-0.9.4/bin/nvim
$ find /nix/store/1haqzsv4n0v0jdpdc3ab3a2if6i79sk9-neovim-0.9.4 | grep editorconfig
/nix/store/1haqzsv4n0v0jdpdc3ab3a2if6i79sk9-neovim-0.9.4/share/nvim/runtime/plugin/editorconfig.lua
It appears my hypothesis is correct.
This derivation is the “wrapped” neovim which calls the “unwrapped” neovim from bin/nvim
, which is really just a bash script.
There’s an rplugin.vim
file
An important note is that, plugins of different languages are loaded differently.
If we look around more we see a couple files called rplugin.vim
which is generated in the wrapper that defines the remote plugin manifest.
This isn’t relevant to lua and viml plugins.
Ok, let’s keep going!
Anytime in a nix file you see pkgs
it likely refers to nixpkgs, so that’s where I’ll look to see where makeNeovimConfig
and wrapNeovimUnstable
are defined.
Both are easy to find with ripgrep or other grep-like tool.
The first is defined in pkgs/applications/editors/neovim/utils.nix
and is just a helper to pre-process the config, and not too interesting.
The other is defined as the following, so the real code is in the referenced file, wrapper.nix
.
wrapNeovimUnstable = callPackage ../applications/editors/neovim/wrapper.nix { };
I’ve read through this file a few times expecting at some point we’d be copying the plugins into the wrapper, but no such code appears to exist. What gives?
First, a confession: I had already found what I needed at this point in my search and fixed the problem.
But when I came back to learn more I realized I got caught in a red herring.
The plugins are not simply copied into the wrapped neovim directory.
The code I was looking for doesn’t exist!
If I had looked closer at bin/nvim
I would’ve found,
exec -a "$0" "/nix/store/hssj9fyb8k5cnxi79dwgm91259gks6xl-neovim-unwrapped-0.9.4/bin/nvim" ... lots of junk ... --cmd "set packpath^=/nix/store/wrqqm1lj550r016hr165lxdd08brzay5-vim-pack-dir" --cmd "set rtp^=/nix/store/wrqqm1lj550r016hr165lxdd08brzay5-vim-pack-dir" "$@"
It turns out packpath
lets you provide a directory for neovim (and vim) to look for packages.
This is how we let neovim know about editorconfig and treesitter.
The treesitter and editorconfig files are ther because they also exist in the unwrapped neovim – they’re included with neovim by default.
The vim-pack-dir derivation is generated by packDir
from a list of packages, which gets defined in pkgs/misc/vim-plugins/vim-utils.nix
(since this is shared between vim and neovim, it goes in the vim directory).
This gets added the bash wrapper in that wrapper.nix
file.
commonWrapperArgs =
# vim accepts a limited number of commands so we join them all
[
"--add-flags" ''--cmd "lua ${providerLuaRc}"''
# (lib.intersperse "|" hostProviderViml)
] ++ lib.optionals (packpathDirs.myNeovimPackages.start != [] || packpathDirs.myNeovimPackages.opt != []) [
"--add-flags" ''--cmd "set packpath^=${vimUtils.packDir packpathDirs}"''
"--add-flags" ''--cmd "set rtp^=${vimUtils.packDir packpathDirs}"''
]
Mystery solved!
In Summary
- The neovim module in home-manager accepts an array of plugins,
- which it uses to generate a neovim config with
makeNeovimConfig
, - where
plugins
becomespacpathDirs
and modified slightly - which it then passes to
wrapNeovimUnstable
, - which takes
pacpathDirs
and passes tovimUtils.packDir
- which combines all the plugins into one derivation called
vim-pack-dir
(each plugin is symlinked within a specific directory structure) - allowing
wrapNeovimUnstable
to append this topackpath
in a version of neovim wrapped in bash - that neovim interprets as a location to find packages