nvf/docs/manual/hacking/additional-plugins.md
NotAShelf 7f3fe5caa5
docs/hacking: describe toLuaObject syntax for mixed tables
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a696449aab94c06827ea4b1d6e6042cc97ee6
2025-08-21 09:45:56 +03:00

7.6 KiB
Raw Permalink Blame History

Adding Plugins

There are two methods for adding new Neovim plugins to nvf. npins is the faster option that should be preferred if the plugin consists of pure Lua or Vimscript code. In which case there is no building required, and we can easily handle the copying of plugin files. Alternative method, which is required when plugins try to build their own libraries (e.g., in Rust or C) that need to be built with Nix to function correctly.

With npins

npins is the standard method of adding new plugins to nvf. You simply need the repository URL for the plugin, and can add it as a source to be built automatically with one command. To add a new Neovim plugin, use npins. For example:

nix-shell -p npins # or nix shell nixpkgs#npins if using flakes

Then run:

npins add --name <plugin name> github <owner> <repo> -b <branch>

::: {.note}

Be sure to replace any non-alphanumeric characters with - for --name. For example

npins add --name lazydev-nvim github folke lazydev.nvim -b main

:::

Once the npins command is done, you can start referencing the plugin as a string.

{
  config.vim.startPlugins = ["lazydev-nvim"];
}

Packaging Complex Plugins

Some plugins require additional packages to be built and substituted to function correctly. For example blink.cmp requires its own fuzzy matcher library, built with Rust, to be installed or else defaults to a much slower Lua implementation. In the Blink documentation, you are advised to build with cargo but that is not ideal since we are leveraging the power of Nix. In this case the ideal solution is to write a derivation for the plugin.

We use buildRustPackage to build the library from the repository root, and copy everything in the postInstall phase.

postInstall = ''
  cp -r {lua,plugin} "$out"

  mkdir -p "$out/doc"
  cp 'doc/'*'.txt' "$out/doc/"

  mkdir -p "$out/target"
  mv "$out/lib" "$out/target/release"
'';

In a similar fashion, you may utilize stdenv.mkDerivation and other Nixpkgs builders to build your library from source, and copy the relevant files and Lua plugin files in the postInstall phase. Do note, however, that you still need to fetch the plugin sources somehow. npins is, once again, the recommended option to fetch the plugin sources. Refer to the previous section on how to use npins to add a new plugin.

Plugins built from source must go into the flake/pkgs/by-name overlay. It will automatically create flake outputs for individual packages. Lastly, you must add your package to the plugin builder (pluginBuilders) function manually in modules/wrapper/build/config.nix. Once done, you may refer to your plugin as a string.

{
  config.vim.startPlugins = ["blink-cmp"];
}

Modular setup options

Most plugins is initialized with a call to require('plugin').setup({...}).

We use a special function that lets you easily add support for such setup options in a modular way: mkPluginSetupOption.

Once you have added the source of the plugin as shown above, you can define the setup options like this:

# in modules/.../your-plugin/your-plugin.nix

{lib, ...}:
let
  inherit (lib.types) bool int;
  inherit (lib.nvim.types) mkPluginSetupOption;
in {
  options.vim.your-plugin = {
    setupOpts = mkPluginSetupOption "plugin name" {
      enable_feature_a = mkOption {
        type = bool;
        default = false;
        # ...
      };

      number_option = mkOption {
        type = int;
        default = 3;
        # ...
      };
    };
  };
}
# in modules/.../your-plugin/config.nix
{lib, config, ...}:
let
  cfg = config.vim.your-plugin;
in {
  vim.luaConfigRC = lib.nvim.dag.entryAnywhere ''
    require('plugin-name').setup(${lib.nvim.lua.toLuaObject cfg.setupOpts})
  '';
}

This above config will result in this Lua script:

require('plugin-name').setup({
  enable_feature_a = false,
  number_option = 3,
})

Now users can set any of the pre-defined option field, and can also add their own fields!

# in user's config
{
  vim.your-plugin.setupOpts = {
    enable_feature_a = true;
    number_option = 4;
    another_field = "hello";
    size = { # nested fields work as well
      top = 10;
    };
  };
}

Details of toLuaObject

As you've seen above, toLuaObject is used to convert our cfg.setupOpts, a Nix attribute set, into Lua tables across the codebase. Here are some rules of the conversion:

  1. Nix null converts to Lua nil

    • foo = null; -> foo = nil
  2. Number and strings convert to their Lua counterparts

  3. Nix attribute sets ({}) and lists ([]) convert into Lua dictionaries and tables respectively. Here is an example of Nix -> Lua conversion.

    • {foo = "bar"} -> {["foo"] = "bar"}

    • ["foo" "bar"] -> {"foo", "bar"}

    • You may also write mixed tables using toLuaObject, using a special syntax to describe a key's position in the table. Let's say you want to get something like {"foo", bar = "baz"} expressed in Lua using Nix. The appropriate Nix syntax to express mixed tables is as follows:

      # Notice the position indicator, "@1"
      { "@1" = "foo"; bar = "baz"; };
      

      This will result in a mixed Lua table that is as follows:

      {"foo", bar = "baz"}
      
  4. You can write raw Lua code using lib.generators.mkLuaInline. This function is part of nixpkgs, and is accessible without relying on nvf's extended library.

    • mkLuaInline "function add(a, b) return a + b end" will yield the following result:
    {
     _type = "lua-inline";
     expr = "function add(a, b) return a + b end";
    }
    

    The above expression will be interpreted as a Lua expression in the final config. Without the mkLuaInline function, you will only receive a string literal. You can use it to feed plugin configuration tables Lua functions that return specific values as expected by the plugins.

    {
       vim.your-plugin.setupOpts = {
         on_init = lib.generators.mkLuaInline ''
           function()
             print('we can write lua!')
           end
         '';
       };
    }
    

Lazy plugins

If the plugin can be lazy-loaded, vim.lazy.plugins should be used to add it. Lazy plugins are managed by lz.n.

# in modules/.../your-plugin/config.nix
{config, ...}: let
  cfg = config.vim.your-plugin;
in {
  vim.lazy.plugins.your-plugin = {
    # Instead of vim.startPlugins, use this:
    package = "your-plugin";

    # ıf your plugin uses the `require('your-plugin').setup{...}` pattern
    setupModule = "your-plugin";
    inherit (cfg) setupOpts;

    # Events that trigger this plugin to be loaded
    event = ["DirChanged"];
    cmd = ["YourPluginCommand"];

    # Plugin Keymaps
    keys = [
      # We'll cover this in detail in the 'keybinds' section
      {
        key = "<leader>d";
        mode = "n";
        action = ":YourPluginCommand";
      }
    ];
  };
}

This results in the following lua code:

require('lz.n').load({
  {
    "name-of-your-plugin",
    after = function()
      require('your-plugin').setup({
        --[[ your setupOpts ]]--
      })
    end,

    event = {"DirChanged"},
    cmd = {"YourPluginCommand"},
    keys = {
      {"<leader>d", ":YourPluginCommand", mode = {"n"}},
    },
  }
})

A full list of options can be found in the vim.lazy.plugins spec on the rendered manual.