Home

Awesome

nv: keep your lazy.nvim config in Lua

<!--toc:start-->

nv is a wrapper around Neovim that makes your Lua configuration nix compatible. It makes the barrier to a working Neovim configuration on nix as small as possible for existing Neovim users.

As the creator of nixCats aptly said, nix is for downloading and Lua is for configuring. I am taking that idea further by transforming your configuration to a nix compatible one at build time. Inside Lua, you have 0 extra dependencies, and as few changes as possible.

nv provides a flake template that you can put inside your Neovim configuration. You have to make a couple minor adjustments to your configuration and specify what dependencies your configuration has and nv will do the rest.

Why

For fun.

Besides that, when I originally was looking into nix, my Neovim configuration was my big blocker. I had spent quite a bit of time on it and I really didn't want to rewrite it in nix, especially since that'd mean I couldn't use it as a normal configuration anymore. Then later I found nixCats, which is a great project, and that helped me to go to nix.

Somewhere along the line, I had some problems with nixCats (completely my own fault), and somewhere around midnight I had a fun idea to just parse my configuration and patch in the plugin directories. A frankly insane 18 hours of programming later, I had some base concepts that worked, and I decided to go for it!

Quick setup

Change loading lazy to:

local set = function(nonNix, nix)
    if vim.g.nix == true then
        return nix
    else
        return nonNix
    end
end

-- Bootstrap lazy.nvim
local load_lazy = set(function()
  local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
  if not (vim.uv or vim.loop).fs_stat(lazypath) then
    local lazyrepo = "https://github.com/folke/lazy.nvim.git"
    local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
    if vim.v.shell_error ~= 0 then
      vim.api.nvim_echo({
        { "Failed to clone lazy.nvim:\n", "ErrorMsg" },
        { out, "WarningMsg" },
        { "\nPress any key to exit..." },
      }, true, {})
      vim.fn.getchar()
      os.exit(1)
    end
  end
  vim.opt.rtp:prepend(lazypath)
end, function()
    -- Prepend the runtime path with the directory of lazy
    -- This means we can call `require("lazy")`
    vim.opt.rtp:prepend([[lazy.nvim-plugin-path]])
end)

-- Actually execute the loading function we set above
load_lazy()

Disable plugins like Mason so they don't download things on nix.

Clone the flake using nix flake init -t github:NicoElbers/nv.

Set luaPath to the directory which contains your init.lua.

Use nixos search to find all the plugins you're using and import them in plugins in the flake.

Use nixos search again to find all runtime dependencies (tree-sitter, lsp) and put them in runtimeDeps.

For more detailed information, look at the section below.

Installation, the long version

Setting up your config

nv does its best to work with existing lazy.nvim configurations, but it's not perfect. The setup you need to however, is minimal. There are 2 main limitations as of now:

  1. Plugins that install files, like mason, don't play nice with nix.
  2. lazy.nvim isn't loaded by lazy.nvim, so we need a special way to be able to load it correctly.

Utilities

For the other 2 limitations you do need to make some changes to your configuration, luckily you still don't have to change a thing when you're not on nix!

The trick to this is very simple. nv does very little magic, but the one bit of magic it does set the global vim.g.nix to true. This allows us to make a very useful utility function:

local set = function(nonNix, nix)
    if vim.g.nix == true then
        return nix
    else
        return nonNix
    end
end

Function inspired by nixCats, thank you BirdeeHub!

We can give a nonNix and a nix value to this function, but what exactly does that do? It means we can assign values, or functions, or anything really based on if we're using nix or not. So for example on nix we can disable mason, or we can have different setup functions on nix and non nix.

<details><summary>How to better integrate the function</summary>

Of course, it's not very nice to have to copy this function over everywhere, for that I personally have a lua/utils.lua file. This file roughly looks like this:

-- Set M to an empty table
local M = {}

-- snip

-- Add boolean values to this table
M.isNix = vim.g.nix == true
M.isNotNix = vim.g.nix == nil

-- Add the set function to this table,
-- we can now call it with require("utils").set(a, b)
function M.set(nonNix, nix)
    if M.isNix then
        return nix
    else
        return nonNix
    end
end

-- snip

return M

That way I can call require("utils") anywhere in my config and have access to set! For an example, see my config.

</details>

Loading lazy.nvim

On nonNix the lazy docs tell you to add this to your config:

-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
  local lazyrepo = "https://github.com/folke/lazy.nvim.git"
  local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
  if vim.v.shell_error ~= 0 then
    vim.api.nvim_echo({
      { "Failed to clone lazy.nvim:\n", "ErrorMsg" },
      { out, "WarningMsg" },
      { "\nPress any key to exit..." },
    }, true, {})
    vim.fn.getchar()
    os.exit(1)
  end
end
vim.opt.rtp:prepend(lazypath)

This is downloading something imperatively, which we want to avoid on nix. Luckily this is super easy to change with our utility. The problem is what do we replace it with?

This is another piece of sort of magic nv does. The Lua string "lazy.nvim-plugin-path" is replaced with the appropriate path to the lazy.nvim plugin. This works because nv provides some default patches if they otherwise wouldn't work out of the box, among these I added the string "lazy.nvim-plugin-path" to be replaced. You can see all default patches in the subPatches.nix file. You can also add your own, as you'll see a bit later.

Beware if you turn off the patchSubs setting, this will no longer work.

Here is how that looks in practice:

local set = function(nonNix, nix)
    if vim.g.nix == true then
        return nix
    else
        return nonNix
    end
end

-- Bootstrap lazy.nvim
local load_lazy = set(function()
  local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
  if not (vim.uv or vim.loop).fs_stat(lazypath) then
    local lazyrepo = "https://github.com/folke/lazy.nvim.git"
    local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
    if vim.v.shell_error ~= 0 then
      vim.api.nvim_echo({
        { "Failed to clone lazy.nvim:\n", "ErrorMsg" },
        { out, "WarningMsg" },
        { "\nPress any key to exit..." },
      }, true, {})
      vim.fn.getchar()
      os.exit(1)
    end
  end
  vim.opt.rtp:prepend(lazypath)
end, function()
    -- Prepend the runtime path with the directory of lazy
    -- This means we can call `require("lazy")`
    vim.opt.rtp:prepend([[lazy.nvim-plugin-path]])
end)

-- Actually execute the loading function we set above
load_lazy()

Dealing with mason and the like

<!-- TODO: make -->

Now that we already have our utility functions this is pretty easy. I'll give my own examples here in the future, for now look at how nixCats does it. If you replace require('nixCatsUtils).lazyadd with our set function, everything works the same.

Setting up the nix part

Inside the directory where you have your configuration do nix flake init -t github:NicoElbers/nv. This creates a flake.nix. Inside this flake you will find the outlines for everything you need.

The main things you need to look out for are plugins, runtimeDeps and luaPath.

The other options are hopefully explained well enough in the template. If not, feel free to make an issue and or pr.

If you want to see what your config looks like, to find errors or just for fun, vim.g.configdir contains the location your patched config is located.

<details><summary>Custom patches</summary>

nv provides you with the possibility to define custom subsitutions (look at how it works for more details). These can be used to change any Lua string into any other Lua string. A specialization of these are plugin subsitutions. These assume that whatever you're replacing is a url and will replace a bit of fluff around it to satisfy lazy.nvim.

In most cases you're gonna want to use a plugin subsitution. You can generate these very easily using the functions provided by patchUtils.nix In the template they are already imported for you. Have a look at them to see how you use them. When you have them generated, you need to put them in customSubs in your flake. After this you should be good.

In the cases that you want to use literal string replacement, a couple of things to note:

</details>

Goals

  1. Make any lazy.nvim configuration nix compatible
  2. Keep Neovim fast
  3. Easy setup for anyone

These are my goals in order.

First and foremost I want you to have no limits within your configuration. You should be able to do whatever you want in non-nix, and do everything within the limits of nix while using nix. I don't want to enforce anything if I can avoid it.

After that I want to keep Neovim fast. Everything should be done at build time, anything done at runtime will slow down neovim which I will avoid at all costs. My 29ms startup should stay 29ms under all circumstances.

Last but not least, setup should be easy. Part of the reason I stared this project is that I had a hard time making my config nix compatible. You shouldn't need to know much if anything about nix to get nv to work.

Limitations

Roadmap

How it works

Patching your config

nv works very differently from other nix Neovim solutions I've seen. Instead of generating Lua from nix configuration or hijacking the package manager, nv patches your configuration at build time paths. Lazy.nvim expects either a url or a directory for any given plugin, so with a bit of clever parsing we can find the urls of your plugins and change them to

But how does it know what urls to change? Wouldn't that be a lot of manual labor? Luckily, no. In nixpkgs vim plugin derivations are all put in one large file in a structured manner. This means that we can parse it quite easily. Combining this with a list of plugins you provide, we can link a url to a store path.

Some plugins, like LuaSnip, don't work, for these exceptions we can make custom patches. If you look at the subPatches.nix file you'll find every custom patch I provide by default (you can disable these by setting patchSubs to false). Doing that in this repository has the nice advantage that once someone finds a faulty plugin, they can upstream their custom patch, and make it available for everyone.

Zig

I chose to do the patching itself in Zig, not nix. Mainly because I've been really liking Zig lately, and I'm not confident I could do complex file parsing in nix. Another advantage of Zig is speed. If I time the patcher on my own config it takes about 0.1 second, which is pretty good I'd say. Right now, that speed doesn't make much of a difference, building the executable takes ~10 seconds (although only happens once) and setting up other things for Neovim takes a few more seconds, but it will make a difference in the future.

One frustration I've heard is that iterating over your configuration is kind of annoying in nix. Rebuilding doesn't take ages, but long enough that it's frustrating. In the future, I plan to provide the patcher executable in some special "iteration" mode, where you can make changes and patch you config yourself. Then having that 0.1 second build time will not be that different from starting up Neovim normally.