A neovim plugin allowing a single neovim configuration with the Lazy plugin manager to be used on NixOS and other platforms. This plugin makes the following arrangement possible:
- On NixOS:
- plugins (and LSP servers/DAP servers/linters/formatters) installed via Nix
- configuration of plugins via Lazy
- lazy loading of plugins via Lazy
- On other platforms:
- the same neovim configuration files you use on NixOS
- plugins installed via Lazy installed via Lazy as normal
- LSP servers/DAP servers/linters/formatters installed via Mason as normal
- configuration via Lazy as normal
- lazy loading of plugins via Lazy as normal
When I switched to NixOS I had an existing neovim configuration using the Lazy plugin manager. Home-Manager for NixOS provides it's own way to manage installation and configuration of neovim plugins, but migrating would be a heavy lift. If I could run NixOS everywhere, I would probably have spent the effort to migrate. I'm not so lucky, so this plugin was created to let me have a portable neovim config that works nicely on and off NixOS.
Normally when using Lazy plugins are configured with a github plugin URL, which Lazy uses to download and install the plugin. However, Lazy also provides a dir
configuration option for the installation of local plugins. By setting the dir
value for each plugin to its location in the nix store, we are able to let Nix manage the installation of our plugins, while letting Lazy manage the configuration.
Lazy-Nix-Helper accepts a table mapping plugin names to nix-store plugin paths as input. The get_plugin_path()
function will return the nix store path corresponding to a given plugin name if the path exists on the system.
Note: the docs list neovim version >= 0.9.0 as a requirement. It will probably work for much earlier versions, but that's all I've tested it on so far.
TODO:
- compatible neovim versions?
- nixos compatibility?
- dependencies?
- neovim io.popen() not available on all platforms
I haven't packaged Lazy-Nix-Helper on NixOS yet, so for now you'll have to package it in your config manually. An example is shown in the NixOS Configuration section below. Additional info can be found here: https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/vim.section.md#what-if-your-favourite-vim-plugin-isnt-already-packaged-what-if-your-favourite-vim-plugin-isnt-already-packaged
The configuration instructions below include code that will install Lazy-Nix-Helper from GitHub when the given nix store path for Lazy-Nix-Helper does not exist.
Default Config
{
lazypath = nil,
friendly_plugin_names = true,
auto_plugin_discovery = false,
input_plugin_table = {}
}
Config Options
-
lazypath
: the default lazypath. must be set in plugin config -
friendly_plugin_names
: when set totrue
provides less-strict plugin name matching for get_plugin_path():- not case sensitive
- treats
-
and_
as identical - add or subtracts
.nvim
from the plugin name as needed
if there is a plugin name collision with these rules applied then lazy-nix-helper will thrown an error. in that case you will have to set this option to false and match plugin names exactly.
-
auto_plugin_discovery
: when set totrue
enables the automatic plugin discovery originally included in Lazy-Nix-Helper. the automatic plugin discovery is a nix anti-pattern and only works for a subset of nix/nixos use cases and config arrangements. this option has been left in place to let early adopters of Lazy-Nix-Helper keep their original configuration, but it should be set tofalse
for all new configurations -
input_plugin_table
: the plugin table mapping plugin names to plugin paths generated by your nix config. instructions for generating this table are provided in the NixOS Configuration section
Lazy-Nix-Helper's own Nix Store Path
The nix store path for the Lazy-Nix-Helper plugin itself cannot be provided by Lazy-Nix-Helper. We can't rely on the built in functionality to find the nix store path because the plugin hasn't been loaded yet, and the plugin can't be loaded without its nix store path, etc.
The recommended way to deal with this is to move your init.lua
configuration into programs.neovim.extraLuaConfig
. Then the nix-store path of Lazy-Nix-Helper can be provided with a variable. See the NixOS Configuration section for more details.
Loading Before Lazy
Because Lazy will resolve plugin configurations before loading any plugins, we must load Lazy-Nix-Helper manually before loading lazy.
Updating init.lua
These instructions will assume you are already installing and loading Lazy with the instructions provided by the Lazy README. If you are doing something different then you may have to adapt these instructions to fit your own config
The Lazy README recommends adding the following to your init.lua
:
-- set lazypath variable to the path where the Lazy plugin will be installed on your system
-- if Lazy is not already installed there, download and install it
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end
-- add the Lazy plugin to the vim runtime
vim.opt.rtp:prepend(lazypath)
-- set mapleader before loading lazy if applicable
vim.g.mapleader = " "
-- install and load plugins from your configuration
require("lazy").setup(plugins, opts)
To update this configuration to work with Lazy-Nix-Helper, we will:
- bootstrap Lazy-Nix-Helper
- add Lazy-Nix-Helper to the vim runtime
- call the Lazy-Nix-Helper
setup()
function - set the lazypath using Lazy-Nix-Helper
Update the configuration as follows:
-- THIS PLUGIN LIST SHOULD BE GENERATED BY YOUR NIX CONFIG. See the NixOS Configuration section for more details on plugins table and lazy_nix_helper_path
local plugins = {
["plugin-name.nvim"] = "nix/store/hash1234-vimplugin-plugin-name.nvim",
...
}
local lazy_nix_helper_path = <lazy_nix_helper/nix/store/path>
-- if we are not on a nix-based system, bootstrap lazy_nix_helper in the same way lazy is bootstrapped
if not vim.loop.fs_stat(lazy_nix_helper_path) then
lazy_nix_helper_path = vim.fn.stdpath("data") .. "/lazy_nix_helper/lazy_nix_helper.nvim"
if not vim.loop.fs_stat(lazy_nix_helper_path) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/b-src/lazy_nix_helper.nvim.git",
lazy_nix_helper_path,
})
end
end
-- add the Lazy-Nix-Helper plugin to the vim runtime
vim.opt.rtp:prepend(lazy_nix_helper_path)
-- call the Lazy-Nix-Helper setup function. pass a default lazypath for non-nix systems as an argument
local non_nix_lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
require("lazy-nix-helper").setup({ lazypath = non_nix_lazypath, input_plugin_table = plugins })
-- get the lazypath from Lazy-Nix-Helper
local lazypath = require("lazy-nix-helper").lazypath()
-- the rest of the configuration is unchanged
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end
-- add the Lazy plugin to the vim runtime
vim.opt.rtp:prepend(lazypath)
-- set mapleader before loading lazy if applicable
vim.g.mapleader = " "
-- install and load plugins from your configuration
require("lazy").setup(plugins, opts)
To provide nix store paths to the rest of the plugins in your configuration, update their configuration as in this example
{
repo/my-cool-plugin.nvim,
dir = require("lazy-nix-helper").get_plugin_path("my-cool-plugin.nvim"),
...
}
Don't forget to update each plugin's dependencies as well
{
repo/my-cool-plugin.nvim,
dir = require("lazy-nix-helper").get_plugin_path("my-cool-plugin.nvim"),
dependencies = {
{
repo/my-cool-plugins-dep,
dir = require("lazy-nix-helper").get_plugin_path("my-cool-plugins-dep"),
...
},
...
},
...
}
Note on Plugin Names in Lazy Dashboard
When you open the lazy dashboard with :Lazy
, each plugin's display name is determined by its directory path. Using nix store paths, this becomes difficult to read because each plugin name is prepended with it's hash. As far as I'm aware there's no way to fix this.
Lazy provides the name
option for plugin config which sets a custom display name for the plugin, but this also:
- sets a custom directory path for the plugin
- uses the custom name to try to find the plugins
setup()
function
Even with a dir
configured, setting name
will break plugin setup.
Mason is a package manager for LSP servers, DAP servers, linters, and formatters. Just like plugin management with Lazy, this conflicts with Nix. The easiest way to keep mason in your config on non-Nix platforms while disabling it on NixOS is to use the provided mason_enabled
function to conditionally enable Mason.
This will require you to separately declare all your LSP servers etc. in your NixOS config, but you were doing that already, right?
There are two parts to this:
- disabling the
mason
(andmason-lspconfig
) plugins - checking that
mason
(andmason-lspconfig
) are enabled before they are used elsewhere in your config
Conditionally enabling mason
Here's an example mason configuration as a dependency of nvim-lspconfig
. Notice that we are using mason_enabled
to conditionally enable both mason
and mason-lspconfig
{
"neovim/nvim-lspconfig",
dir = require("lazy-nix-helper").get_plugin_path("nvim-lspconfig"),
dependencies = {
{
"williamboman/mason.nvim",
enable = require("lazy-nix-helper").mason_enabled(),
...
},
{
"williamboman/mason-lspconfig.nvim",
enable = require("lazy-nix-helper").mason_enabled(),
...
},
...
},
...
}
Note that specifying the dir
parameter is not necessary here since these plugins will be disabled in NixOS.
Conditionally calling mason
This part is harder to give examples for because there are a lot of ways you could be setting this up.
I based my original neovim configuration on kickstart.nvim, which had checks that mason
and mason-lspconfig
were enabled already built again. This was before they migrated to using Lazy, and it looks like this has changed sense.
Some general advice:
- You can still use the
mason_enabled()
function to gatemason
ormason-lspconfig
calls in your configuration - Make sure that
lsp-config.setup()
function is still called for each server
After making these changes in my own config LSP servers were working for me on NixOS.
Caveat for Linters and Formatters
In my own config I don't use linters or formatters provided by mason. I prefer to handle that on a per-project basis or use tools provided by the language. I haven't tested this plugin with a configuration that includes linters or formatters, and can't confirm that they work with this setup.
There are a lot of different ways you might set up your NixOS configuration. This section will give configuration examples using home-manager. You may need to adapt this to fit your own system configuration.
The necessary components are:
- Package lazy-nix-helper yourself. See these instructions for additional information https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/vim.section.md#what-if-your-favourite-vim-plugin-isnt-already-packaged-what-if-your-favourite-vim-plugin-isnt-already-packaged
- Put your existing
init.lua
withinprograms.neovim.extraLuaConfig
. - Update your
init.lua
to provide the nix store path of Lazy-Nix-Helper - Generate the plugins table
- Include the rest of your config files with
xdg.configFile
.
neovim.nix
module:
{ pkgs, config, ... }:
let
lazy-nix-helper-nvim = pkgs.vimUtils.buildVimPlugin {
name = "lazy-nix-helper.nvim";
src = pkgs.fetchFromGitHub {
owner = "b-src";
repo = "lazy-nix-helper.nvim";
rev = "<git commit hash>";
hash = "<sha256 of archive of git commit>";
};
};
sanitizePluginName = input:
let
name = lib.strings.getName input;
intermediate = lib.strings.removePrefix "vimplugin-" name;
result = lib.strings.removePrefix "lua5.1-" intermediate;
in result;
pluginList = plugins: lib.strings.concatMapStrings (plugin: " [\"${sanitizePluginName plugin.name}\"] = \"${plugin.outPath}\",\n") plugins;
in
{
xdg.configFile."nvim/lua" = {
source = ./neovim_config/lua;
recursive = true;
};
programs.neovim = {
enable = true;
...
extraPackages = with pkgs; [
<lsps, etc.>
];
plugins = with pkgs.vimPlugins; [
lazy-nix-helper-nvim
lazy-nvim
<other plugins>
];
extraLuaConfig = ''
local plugins = {
${pluginList config.programs.neovim.plugins}
}
local lazy_nix_helper_path = "${lazy-nix-helper-nvim}"
if not vim.loop.fs_stat(lazy_nix_helper_path) then
lazy_nix_helper_path = vim.fn.stdpath("data") .. "/lazy_nix_helper/lazy_nix_helper.nvim"
if not vim.loop.fs_stat(lazy_nix_helper_path) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/b-src/lazy_nix_helper.nvim.git",
lazy_nix_helper_path,
})
end
end
-- add the Lazy Nix Helper plugin to the vim runtime
vim.opt.rtp:prepend(lazy_nix_helper_path)
-- call the Lazy Nix Helper setup function
local non_nix_lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
local lazy_nix_helper_opts = { lazypath = non_nix_lazypath, input_plugin_table = plugins }
require("lazy-nix-helper").setup(lazy_nix_helper_opts)
-- get the lazypath from Lazy Nix Helper
local lazypath = require("lazy-nix-helper").lazypath()
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
<additional config in init.lua>
'';
};
}
After moving your init.lua
directly into your NixOS config and sourcing the rest of your dotfiles within your NixOS config, you should use the built output in ~/.config/nvim
as the source for sharing your dotfiles with a non-NixOS system.
I have not tested Lazy-Nix-Helper with:
- Nix-installed neovim/plugins on a non-NixOS system
- Nix-darwin
Besides Lazy-Nix-Helper there are other tools and strategies you might choose to manage your neovim configuration under NixOS:
- Home-Manager provides options for installing and configuring neovim plugins
- NixVim is a neovim distribution built around Nix modules. It provides nix-style configuration options for every neovim configuration option as well as options for many plugins
TODO: include some examples
- Using your existing config as-is. Depending on how complicated your current config is and what it includes, you may be able to copy your dotfiles and have everything work out of the box. The main downside is that you lose the reproducability benefits of nix by using a different package manager
Use the :Lazy
command to open the Lazy dashboard. The source directory is listed for each plugin
Run the list_input_plugins
function manually: lua require("lazy-nix-helper").list_input_plugins()
Run the list_discovered_plugins
function manually: lua require("lazy-nix-helper").list_discovered_plugins()
Run the get_plugin_path
function manually: :lua print(require("lazy-nix-helper").get_plugin_path("<plugin-name>"))
Run the print_environment_info
function manually to see:
- if Lazy-Nix-Helper thinks you're on NixOS or not
- if Lazy-Nix-Helper thinks nix-store is installed or not (only relevant for legacy automatic plugin discovery)
lua require("lazy-nix-helper").print_environment_info()
Running the automated tests requires Plenary
The test setup script will automatically download plenary into the test_dependencies
directory
From the repo root, run make test
to run the full suite of tests.
Linting requires Luacheck
Auto-formatting requires Stylua
make lint
will run both auto-formatting and linting.
make format
will run only auto-formatting.
make check
will run only linting.