Skip to content
/ lze Public

A lazy-loading library for neovim, inspired by, but different from, nvim-neorocks/lz.n

License

Notifications You must be signed in to change notification settings

BirdeeHub/lze

Repository files navigation

πŸ¦₯ lze

Neovim Lua LuaRocks

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

It is intended to be used

  • by users of plugin managers that don't provide a convenient API for lazy-loading.
  • by plugin managers, to provide a convenient API for lazy-loading.

❓ Is this lz.n?

Nope. I quite like lz.n. I have contributed to lz.n many times.

In fact, I have even contributed to lz.n after this plugin has been created.

lz.n is a great plugin. I think having the handlers manage the entire state of lz.n is an elegant solution.

But it also meant I had to be more careful when writing handlers, and lz.n to that effect has provided an API to make this easier.

I wanted more things to be guaranteed, and I started drafting this.

This is my take on lz.n.

The core has been entirely rewritten and it handles its state entirely differently.

It shares some code where handlers parse their specs, otherwise it works entirely differently, but with a largely compatible plugin spec

Which one is better? Hard to say.

Neither is appreciably faster than the other.

The plugin specs are basically the same, with 1-3 more fields thrown in that I have written lz.n equivalents for.

Its basically down to which design of handlers you prefer.

However, import specs can only import a single module rather than a whole directory.

Why?

lze actually treats your list of specs as a list. If your startup plugins have not been given a priority, they load in the order passed in.

What order should the directory be imported in? I left it up to you.

Handlers are very different. They have much less responsibility to manage state for lze.

Handlers can achieve all the same things, but in a different way. Possibly more, but I am not sure.

Why does the readme still say it is a dead simple library?

The core of lze is simply a read-only table. You queue up a plugin, a handler loads it when you tell it to, it gets replaced with false.

Handlers have 1 chance to prevent a plugin from entering, or modify it before it enters if active for that spec, (none of the default handlers need this).

Once it has been entered, it will remain there until it has been loaded via a call to require('lze').trigger_load(name) (or a list of names).

You can only add it to the queue again after it has been loaded, and specifically allow it to be added again.

That's basically it. The handlers call trigger_load with some names on some sort of event, lze loads it if its in the table, and if not, it returns the skipped ones, by default, warning if it wasn't found at all.

🌟 Features

  • API for lazy-loading plugins on:
    • Events (:h autocmd-events)
    • FileType events
    • Key mappings
    • User commands
    • Colorscheme events
    • Other plugins
    • whatever you can write a custom handler for
  • Works with:
    • Neovim's built-in :h packpath (:h packadd)
    • Any plugin manager that supports manually lazy-loading plugins by name
  • Configurable in multiple files

πŸŒ” Introduction

lze 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.

If attempting lazy loading via autocommands, it can get very verbose when you wish to load a plugin on multiple triggers.

This greatly simplifies that process, and is easy to extend with your own custom fields via custom handlers, the same mechanism through which the builtin handlers are created.

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 nvim-neorocks "DO's and DONT's" guide for plugin developers.

Regardless, some authors may not have the capacity or knowledge 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.

🌌 Philosophy

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

πŸ’€ Comparison with lazy.nvim

  • lze is not a plugin manager, but focuses on lazy-loading only. It is intended to be used with (or by) a plugin manager.
  • The feature set is minimal, to reduce code complexity and simplify the API. For example, the following lazy.nvim features are out of scope:
    • Merging multiple plugin specs for a single plugin (primarily intended for use by Neovim distributions).
    • lazy.vim completely disables and takes over Neovim's built-in loading mechanisms, including adding a plugin's API (lua, autoload, ...) to the runtimepath. lze doesn't. Its only concern is plugin initialization, which is the bulk of the startup overhead.
    • Automatic lazy-loading of colorschemes. lze provides a colorscheme handler in the plugin spec.
    • Heuristics for determining a main module and automatically calling a setup() function.
    • Abstractions for plugin configuration with an opts table. lze provides simple hooks that you can use to specify when to load configurations.
    • Heuristics for automatically loading plugins on require. You can lazy-load on require with some configuration however!
    • Features related to plugin management.
    • Profiling tools.
    • UI.
  • Some configuration options are different.

πŸ“ Requirements

  • Neovim >= 0.10.0

πŸ”§ Configuration

You can override the function used to load plugins. lze has the following defaults:

vim.g.lze = {
    ---@type fun(name: string)
    load = vim.cmd.packadd,
    ---@type boolean
    verbose = true,
}

If vim.g.lze.verbose is false it will not print a warning in cases of duplicate and missing plugins.

πŸ“š Usage

require("lze").load(plugins)

Tip

You can call require('lze').load() as many times as you wish.

Important

Since merging configs is out of scope, calling load() with conflicting plugin specs is not supported. It will prevent you from doing so, and return the list of duplicate names

Plugin spec

Loading hooks

Property Type Description lazy.nvim equivalent
[1] string REQUIRED. The plugin's name (not the module name). This is the directory name of the plugin in the packpath and is usually the same as the repo name of the repo it was cloned from. name1
enabled? boolean or fun():boolean When false, or if the function returns nil or false, then this plugin will not be included in the spec. enabled
beforeAll? fun(lze.Plugin) Always executed upon calling require('lze').load(spec) before any plugin specs from that call are triggered to be loaded. init
before? fun(lze.Plugin) Executed before a plugin is loaded. None
after? fun(lze.Plugin) Executed after a plugin is loaded. config
priority? number Only useful for start plugins (not lazy-loaded) added within the same require('lze').load(spec) call to force loading certain plugins first. Default priority is 50. priority
load? fun(string) Can be used to override the vim.g.lze.load(name) function for an individual plugin. (default is vim.cmd.packadd(name))2 None.
allow_again? boolean or fun():boolean When a plugin has ALREADY BEEN LOADED, true would allow you to add it again. No idea why you would want this outside of testing. None.
lazy? boolean Using a handler's field sets this automatically, but you can set this manually as well. lazy

Lazy-loading triggers provided by the default handlers

Property Type Description lazy.nvim equivalent
event? string 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
cmd? string or string[] Lazy-load on command. cmd
ft? string or string[] Lazy-load on filetype. ft
keys? string or string[] or lze.KeysSpec[] Lazy-load on key mapping. keys
colorscheme? string or string[] Lazy-load on colorscheme. None. lazy.nvim lazy-loads colorschemes automatically3.
dep_of? string or string[] Lazy-load before another plugin but after its before hook. Accepts a plugin name or a list of plugin names. None but is sorta the reverse of the dependencies key of the lazy.nvim plugin spec

Extra fields

There are also 2 more optional handlers you may add to your spec.

Property Type Description lazy.nvim equivalent
on_plugin? string or string[] Lazy-load after another plugin but before its after hook. Accepts a plugin name or a list of plugin names. None.
on_require? string or string[] Accepts a top-level lua module name or a list of top-level lua module names. Will load when any submodule of those listed is required None. lazy.nvim does this automatically.

To add them, before you call load use:

require("lze").register_handlers(require('lze.x'))

or individually with:

require("lze").register_handlers(require('lze.x.on_require'))
-- or
require("lze").register_handlers(require('lze.x.on_plugin'))
-- or
require("lze").register_handlers({
  require('lze.x.on_plugin'),
  require('lze.x.on_require'),
})

User events

  • DeferredUIEnter: Triggered when require('lze').load() is done and after UIEnter. Can be used as an event to lazy-load plugins that are not immediately needed for the initial UI4.

Plugins with after directories

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 not great either and we suggest opening an issue or submitting a PR to fix any of these issues upstream.

Note

  • This does not work with plugins that rely on after/plugin, such as many nvim-cmp sources, because Neovim's :h packadd does not source after/plugin scripts after startup has completed. We recommend bundling such plugins with their extensions, or sourcing the after scripts manually. In the spirit of the UNIX philosophy, lze does not provide any functions for sourcing plugin scripts. For sourcing after/plugin directories manually, you can use rtp.nvim. Here is an example.

Tip

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

Examples

require("lze").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" } },
    },
}
paq-nvim example
require "paq" {
    { "nvim-telescope/telescope.nvim", opt = true }
    { "NTBBloodBatch/sweetie.nvim", opt = true }
}

require("lze").load {
    {
        "telescope.nvim",
        cmd = "Telescope",
    },
    {
        "sweetie.nvim",
        colorscheme = "sweetie",
    },
}
Nix examples
  • Home Manager:
programs.neovim = {
  enable = true;
  plugins = with pkgs.vimPlugins [
    {
      plugin = lze;
      config = ''
        -- optional, add extra handlers
        -- require("lze").register_handlers(require('lze.x'))
      '';
      type = "lua";
    }
    {
      plugin = telescope-nvim;
      config = ''
        require("lze").load {
          "telescope.nvim",
          cmd = "Telescope",
        }
      '';
      type = "lua";
      optional = true;
    }
    {
      plugin = sweetie-nvim;
      config = ''
        require("lze").load {
          "sweetie.nvim",
          colorscheme = "sweetie",
        }
      '';
      type = "lua";
      optional = true;
    }
  ];
};

While the home manager syntax is also accepted by nixCats anywhere we can add plugins, nixCats allows you to configure it in your normal lua files.

Add it to your startupPlugins set as shown below, put the desired plugins in optionalPlugins so they don't auto-load, then configure as in the regular examples wherever you want in your config.

# in your categoryDefinitions
categoryDefinitions = { pkgs, settings, categories, name, ... }: {
# :help nixCats.flake.outputs.categories
  startupPlugins = with pkgs.vimPlugins; {
    someName = [
      # in startupPlugins so that it is available
      lze
    ];
  };
  optionalPlugins = with pkgs.vimPlugins; {
    someName = [
      # the plugins you wish to load via lze
    ];
    # you can name the categories whatever you want,
    # the important thing is,
    # optionalPlugins is for lazy loading via packadd
  };
};
# see :help nixCats.flake.outputs.packageDefinitions
packageDefinitions = {
  nvim = {pkgs , ... }: {
    # see :help nixCats.flake.outputs.settings
    settings = {/* your settings */ };
    categories = {
      # don't forget to enable it for the desired package!
      someName = true;
      # ... your other categories here
    };
  };
};
# ... the rest of your nix where you call the builder and export packages
  • Not on nixkgs-unstable?

If your neovim is not on the nixpkgs-unstable channel, vimPlugins.lze will not yet be in nixpkgs for you. You may instead get it from this flake!

# in your flake inputs:
inputs.lze.url = "github:BirdeeHub/lze";

Then, pass your config your inputs from your flake, and retrieve lze with inputs.lze.packages.${pkgs.system}.default:

Structuring Your Plugins

The import spec of lze allows for importing a single lua module, unlike lz.n or lazy.nvim, where it imports an entire directory.

That module may return a list of specs, which means it can also return a list of import specs.

This way, you get to choose the order, and can have files in that directory that are not imported if you wish.

require("lze").load("plugins")

where lua/plugins in your config contains an init.lua with something like,

return {
  {
    "undotree",
    cmd = {
      "UndotreeToggle",
      "UndotreeHide",
      "UndotreeShow",
      "UndotreeFocus",
      "UndotreePersistUndo",
    },
    keys = { { "<leader>U", "<cmd>UndotreeToggle<CR>", mode = { "n" }, desc = "Undo Tree" }, },
    before = function(_)
      vim.g.undotree_WindowLayout = 1
      vim.g.undotree_SplitWidth = 40
    end,
  },
  { import = "plugins.afile" },
  { import = "plugins.another" },
  { import = "plugins.another_file" },
  { import = "plugins.yet_another_file" },
}

where the imported files would return plugin specs as shown above.

πŸ”Œ API

Custom handlers

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

Warning

You must register ALL handlers before calling require('lze').load, because they will not be retroactively applied to the load calls that occur before they are registered.

---@param handlers lze.Handler[]|lze.Handler|lze.HandlerSpec[]|lze.HandlerSpec
---@return string[] handlers_registered
require("lze").register_handlers({
    require("my_handlers.module1"),
    require("my_handlers.module2"),
    {
        handler = require("my_handlers.module3"),
        enabled = true,
    },
})

You may call this function multiple times, each call will append the new handlers (if enabled) to the end of the list.

The handlers define the fields you may use for lazy loading, with the fields like ft and event that exist in the default plugin spec being defined by the default handlers and the extra, optional ones being defined here.

The order of this list of handlers is important.

It is the same as the order in which their hooks are called.

If you wish to redefine a default handler, or change the order in which the default handlers are called, there exists a require('lze').clear_handlers() function for this purpose. It returns the removed handlers.

Here is an example of how you would add a custom handler BEFORE the default list of handlers:

local default_handlers = require('lze').clear_handlers() -- clear_handlers removes ALL handlers
-- and now we can register them in any order we want.
require("lze").register_handlers(require("my_handlers.b4_defaults"))
require("lze").register_handlers(default_handlers)

Again, this is important:

Warning

You must register ALL handlers before calling require('lze').load, because they will not be retroactively applied to the load calls that occur before they are registered.

lze.HandlerSpec

Property Type Description
handler lze.Handler the lze.Handler you wish to add
enabled? boolean? or fun():boolean? determines at time of registration if the handler should be added or not. Defaults to true

Writing Custom Handlers

lze.Handler

Property Type Description
spec_field string the lze.PluginSpec field used to configure the handler
add? fun(plugin: lze.Plugin) called once for each handler before any plugin has been loaded. Tells your handler about each plugin so you can set up a trigger for it if your handler was used.
before? fun(name: string) called after each plugin spec's before hook, and before its load hook
after? fun(name: string) called after each plugin spec's load hook and before its after hook
modify? fun(plugin: lze.Plugin): lze.Plugin This function is called before a plugin is added to state. It is your one chance to modify the plugin spec, it is active only if your spec_field was used in that spec, and is called in the order the handlers have been added.
set_lazy? boolean Whether using this handler's field should have an effect on the lazy setting. True or nil is true. Default: nil
post_def? fun() For adding custom triggers such as the event handler's DeferredUIEnter event, called at the end of require('lze').load

Your handler first has a chance to modify the parsed plugin spec before it is loaded into the state of lze.

The modify field of a handler will only be called if that handler's spec_field was used in that plugin spec.

They will be called in the order in which your handlers are registered, and none of the builtin handlers use it.

Then, your handler will have a chance to add plugins to its list to trigger, via its add function, which is called before any plugins have been loaded in that require('lze').load call.

Your handler will then decide when to load a plugin and run its associated hooks using the trigger_load function.

  ---@overload fun(plugin_name: string | string[]): string[]
  require('lze').trigger_load

trigger_load will resist being called multiple times on the same plugin name. It will return the list of names it skipped.

There exists a function to check if a plugin is available to be loaded.

require('lze').state(name) will return true if the plugin is ready to be loaded, false if already loaded or currently being loaded, and nil if it was never added.

Less performant, but more informative:

For debugging purposes, or if necessary, you may use the table access form require('lze').state[name] which will return a COPY of the internal state of the plugin.

You should already have a copy of the plugin via your handler's add function so you shouldn't ever NEED to get a copy. But it is nice for troubleshooting.

Tip

You should delete the plugin from your handler's state in either the before or after hooks so that you don't have to carry around unnecessary state and increase your chance of error and your memory usage. However, not doing so would not cause any bugs in lze. It just might let you call trigger_load multiple times unnecessarily

πŸ’š Contributing

All contributions are welcome! See CONTRIBUTING.md.

πŸ“– License

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

Footnotes

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

  2. for example, lazy-loading cmp sources will require you to source its after/plugin file, as packadd does not do this automatically for you. ↩

  3. 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. ↩

  4. This is equivalent to lazy.nvim's VeryLazy event. ↩