Home

Awesome

<!-- markdownlint-disable MD028 -->

:sloth: lz.n

Neovim Lua LuaRocks

A dead simple lazy-loading Lua library for Neovim plugins.

It is intended to be used

[!NOTE]

Should I lazy-load plugins?

It should be a plugin author's responsibility to ensure their plugin doesn't unnecessarily impact startup time, not yours!

See our "DO's and DONT's" guide for plugin developers.

Regardless, the current status quo is horrible, and some authors may not have the will or capacity to improve their plugins' startup impact.

If you find a plugin that takes too long to load, or worse, forces you to load it manually at startup with a call to a heavy setup function, consider opening an issue on the plugin's issue tracker.

:star2: Features

:moon: Introduction

lz.n provides abstractions for lazy-loading Neovim plugins, with an API that is loosely based on lazy.nvim, but reduced down to the very basics required for lazy-loading only.

:milky_way: Philosophy

lz.n is designed based on the UNIX philosophy: Do one thing well.

:zzz: Comparison with lazy.nvim

:pencil: Requirements

:wrench: Configuration

You can override the function used to load plugins. lz.n has the following default:

vim.g.lz_n = {
    ---@type fun(name: string)
    load = vim.cmd.packadd,
}

:books: Usage

require("lz.n").load(plugins)

[!TIP]

[!IMPORTANT]

Since merging configs is out of scope, calling load() with conflicting plugin specs is not supported.

Plugin spec

<!-- markdownlint-disable MD013 -->
PropertyTypeDescriptionlazy.nvim equivalent
[1]stringThe plugin's name (not the module name). This is what is passed to the load(name) function.name1
enabledboolean? or fun():booleanWhen false, or if the function returns false, then this plugin will not be included in the spec.enabled
beforeAllfun(lz.n.Plugin)?Always executed before any plugins are loaded.init
beforefun(lz.n.Plugin)?Executed before a plugin is loaded.None
afterfun(lz.n.Plugin)?Executed after a plugin is loaded.config
eventstring? or {event?:string|string[], pattern?:string|string[]}\ or string[]Lazy-load on event. Events can be specified as BufEnter or with a pattern like BufEnter *.lua.event
cmdstring? or string[]Lazy-load on command.cmd
ftstring? or string[]Lazy-load on filetype.ft
keysstring? or string[] or lz.n.KeysSpec[]Lazy-load on key mapping.keys
colorschemestring? or string[]Lazy-load on colorscheme.None. lazy.nvim lazy-loads colorschemes automatically2.
prioritynumber?Only useful for start plugins (not lazy-loaded) to force loading certain plugins first. Default priority is 50.priority
loadfun(string)?Can be used to override the vim.g.lz_n.load() function for an individual plugin.None.
<!-- markdownlint-enable MD013 -->

User events

Plugin dependencies

This library does not provide a lz.n.PluginSpec field like lazy.nvim's dependencies. The rationale behind this is that you shouldn't need it. Instead, you can utilise the trigger_load function in a before or after hook.

However, we generally do not recommend this approach. Most plugins primarily rely on the Lua libraries of other plugins, which can be added to the :h package.path without any noticeable impact on startup time.

Relying on another plugin's plugin or after/plugin scripts is considered a bug, as Neovim's built-in loading mechanism does not guarantee initialisation order. Requiring users to manually call a setup function is an anti pattern. Forcing users to think about the order in which they load plugins that extend or depend on each other is even worse. We strongly suggest opening an issue or submitting a PR to fix this upstream. However, if you're looking for a temporary workaround, you can use trigger_load in a before or after hook, or bundle the relevant plugin configurations.

[!NOTE]

[!TIP]

We recommend care.nvim as a modern alternative to nvim-cmp.

Examples

require("lz.n").load {
    {
        "neo-tree.nvim",
        keys = {
            -- Create a key mapping and lazy-load when it is used
            { "<leader>ft", "<CMD>Neotree toggle<CR>", desc = "NeoTree toggle" },
        },
        after = function()
            require("neo-tree").setup()
        end,
    },
    {
        "crates.nvim",
        -- lazy-load when opening a toml file
        ft = "toml",
    },
    {
        "sweetie.nvim",
        -- lazy-load when setting the `sweetie` colorscheme
        colorscheme = "sweetie",
    },
    {
        "vim-startuptime",
        cmd = "StartupTime",
        before = function()
            -- Configuration for plugins that don't force you to call a `setup` function
            -- for initialization should typically go in a `before`
            --- or `beforeAll` function.
            vim.g.startuptime_tries = 10
        end,
    },
    {
        "care.nvim",
        -- load care.nvim on InsertEnter
        event = "InsertEnter",
    },
    {
        "dial.nvim",
        -- lazy-load on keys. -- Mode is `n` by default.
        keys = { "<C-a>", { "<C-x>", mode = "n" } },
    },
}
<!-- markdownlint-disable --> <details> <summary> <b><a href="https://github.com/savq/paq-nvim">paq-nvim</a> example</b> </summary>
require "paq" {
    { "nvim-telescope/telescope.nvim", opt = true }
    { "NTBBloodBatch/sweetie.nvim", opt = true }
}

require("lz.n").load {
    {
        "telescope.nvim",
        cmd = "Telescope",
    },
    {
        "sweetie.nvim",
        colorscheme = "sweetie",
    },
}
</details> <details> <summary> <b><a href="https://wiki.nixos.org/wiki/Neovim">Nix (Home Manager) example</a></b> </summary>
programs.neovim = {
  enable = true;
  plugins = with pkgs.vimPlugins [
    lz-n
    {
      plugin = pkgs.vimPlugins.telescope-nvim;
      config = ''
        require("lz.n").load {
          "telescope.nvim",
          cmd = "Telescope",
        }
      '';
      type = "lua";
      optional = true;
    }
    {
      plugin = pkgs.vimPlugins.sweetie-nvim;
      config = ''
        require("lz.n").load {
          "sweetie.nvim",
          colorscheme = "sweetie",
        }
      '';
      type = "lua";
      optional = true;
    }
  ];
};
</details> <!-- markdownlint-restore -->

Structuring Your Plugins

As is the case with lazy.nvim, you can also split your plugin specs into multiple files. Instead of passing a spec table to load(), you can use a Lua module. The function will merge specs from the module and any top-level sub-modules together in the final spec, so it is not needed to add require calls in your main plugin file to the other files.

Example:

require("lz.n").load("plugins")
return {
    { "sweetie.nvim" },
    { "telescope.nvim", cmd = "Telescope" },
}

Example structure:

── nvim
  ├── lua
  │  └── plugins # Your plugin specs go here.
  │     └── init.lua # Optional top-level module returning a list of specs
  │     └── neorg.lua # Single spec
  │     └── telescope/init.lua # Single spec
  ├── init.lua

Or

── nvim
  ├── lua
  │  └── plugins.lua # Optional top-level module returning a list of specs
  ├── init.lua

:electric_plug: API

Custom handlers

You may register your own handlers to lazy-load plugins via other triggers not already covered by the plugin spec.

You should register all handlers before calling require('lz.n').load, because they will not be retroactively applied to the load calls that occur before they are registered.

The register_handler function returns a boolean that indicates success.

---@param handler lz.n.Handler
---@return boolean success
require("lz.n").register_handler(handler)

lz.n.Handler

<!-- markdownlint-disable MD013 -->
PropertyTypeDescription
spec_fieldstringThe lz.n.PluginSpec field used to configure the handler
parsefun(plugin: lz.n.Plugin, spec: unknown)?Parse a spec and add it to the passed in plugin
addfun(plugin: lz.n.Plugin)Adds a plugin to the handler
delfun(name: string)Removes a plugin from the handler by name
lookupfun(name: string): lz.n.Plugin?Lookup a plugin managed by this handler by name
post_loadfun()?Ran once after each require('lz.n').load call, for handlers to create custom triggers such as the event handler's DeferredUIEnter event
<!-- markdownlint-enable MD013 -->

To manage handler state safely, ensuring trigger_load can be invoked from within a plugin's hooks, it is recommended to use the :h lz.n.handler.state module.

[!TIP]

For some examples, look at

Lua API

The following Lua functions are part of the public API.

[!WARNING]

If you use internal functions or modules that are not listed here, things may break without a major version bump.

trigger_load

You can manually load a plugin and run its associated hooks using the trigger_load function:

  ---@overload fun(plugin: lz.n.Plugin | lz.n.Plugin[])
  ---@overload fun(plugin_name: string | string[], opts: lz.n.lookup.Opts): string[]
  require('lz.n').trigger_load

The function provides two overloads, each suited for different use cases:

  1. Stateless version:
    • Usage: trigger_load(plugin: lz.n.Plugin)
    • Intended for: Use by a lz.n.Handler
    • Description: This version should be used when working with lz.n.Handler instances to maintain referential transparency. Each handler has full authority over its internal state, ensuring it remains isolated and unaffected by external influences5, thereby preventing multiple sources of truth.
  2. Stateful version:
    • Usage: trigger_load(plugin_name: string | string[], opts?: lz.n.lookup.Opts)
    • Returns: A list of plugin names that were skipped (empty if all plugins were loaded).
    • Intended for: Scenarios where handler state is unknown or inaccessible, such as in before or after hooks.
    • Description: This version allows you to load plugins by name. It searches through the handlers, querying their lookup functions to identify an appropriate plugin, and returns the first match. You can fine-tune the search process by providing a lz.n.lookup.Opts table.

lookup

To lookup a plugin that is pending to be loaded by name, use:

  ---@type fun(name: string, opts: lz.n.lookup.Opts):lz.n.Plugin?
  require('lz.n').lookup

The lookup, as well as trigger_load(string|string[]) can be fine-tuned with a lz.n.lookup.Opts table:

---@class lz.n.lookup.Opts
---
--- The handlers to include in the search (filtered by `spec_field`)
--- In case of multiple filters, the order of the filter list
--- determines the order in which handlers' `lookup` functions are called.
---@field filter string | string[]

:green_heart: Contributing

All contributions are welcome! See CONTRIBUTING.md.

:book: License

This library is licensed according to GPL version 2 or (at your option) any later version.

<!-- MARKDOWN LINKS & IMAGES -->

Footnotes

  1. In contrast to lazy.nvim's name field, a lz.n.PluginSpec's name is not optional. This is because lz.n is not a plugin manager and needs to be told which plugins to load.

  2. The reason this library doesn't lazy-load colorschemes automatically is that it would have to know where the plugin is installed in order to determine which plugin to load.

  3. This is equivalent to lazy.nvim's VeryLazy event.

  4. It does not merge multiple specs for the same plugin from different files.

  5. Until the handler is instructed to stop tracking a loaded plugin via its del function.