Skip to content

Custom handler examples

Birdee edited this page Aug 31, 2024 · 11 revisions

Recipe

Writing a custom lz.n.Handler boils down to the following recipe:

  • Declare a lz.n.PluginSpec field that will be used to configure how the handler lazy-loads a plugin.
  • Keep track of plugins that have such a field, with the add function.
  • Provide a way for lz.n to check if a plugin is pending to be lazy-loaded by this handler, with the lookup function.
  • Implement the lazy-loading logic.
  • Provide a way for lz.n to remove a plugin from the handler if it has been loaded.

Tip

To minimise a handler's impact on startup time, the add function should do as little as possible, i.e.

  • Check if a plugin has a matching field.
  • If so, update the handler's state.

Defer any other logic to when the plugin is loaded.

Examples

The following examples are out of scope for lz.n, but are listed here for documentation purposes. Feel free to use them in your configs.

Auto-load when requireing a module.

This is adapted from #25.

Warning

If you're using Neovim's built-in loading mechanism, lz-n-auto-require is probably better suited for this use case.

This example is brittle, as a plugin may add, remove or rename top-level modules after an update.

---@class lz.n.ReqPluginSpec: lz.n.PluginSpec
---@field on_require string[]

---@class lz.n.ReqPlugin: lz.n.Plugin
---@field on_require string[]

---@type lz.n.handler.State
local state = require("lz.n.handler.state").new()

---@type lz.n.Handler
local M = {
    spec_field = "on_require",
    ---@param plugin lz.n.ReqPlugin
    add = function(plugin)
        if not plugin.on_require then
            return
        end
        state.insert(plugin)
    end,
    del = state.del,
    lookup = state.lookup_plugin,
}

local trigger_load = require("lz.n").trigger_load

-- How we search for and load our plugins.
---@param mod_path string
---@return boolean
local function call(mod_path)
    local triggered_load = false
    ---@param plugin lz.n.ReqPlugin
    state.each_pending(function(plugin)
        local on_req = plugin.on_require
        ---@type string[]
        local mod_paths = {}
        if type(on_req) == "table" then
            ---@cast on_req string[]
            mod_paths = on_req
        elseif type(on_req) == "string" then
            mod_paths = { on_req }
        end
        local has_mod = vim.iter(mod_paths):any(function(path)
            return vim.startswith(mod_path, path)
        end)
        if has_mod then
            trigger_load(plugin)
            triggered_load = true
        end
    end)
    return triggered_load
end

--- Override `require` to search for plugins to lazy-load.
local oldrequire = require
require("_G").require = function(mod_path)
    local ok, value = pcall(oldrequire, mod_path)
    if ok then
        return value
    end
    package.loaded[mod_path] = nil
    if call(mod_path) == true then
        return oldrequire(mod_path)
    end
    error(value)
end

return M

After registering the handler, you can then use it like this:

require("lz.n").load({
    "plenary.nvim",
    -- top-level module
    on_require = {"plenary"}
})

Dependency handler

This custom handler will allow you to mark a spec to "load_before" another one.

It will load the spec after the before hook and before the load hook of any of the named plugins.

If any of the plugins listed to "load_before" on this spec have already been loaded, it will instead be loaded immediately.

For the reasons here this is RARELY necessary.

But when it is, this is very nice!

Usage:

If you put the below handler in lua/handlers/load_before.lua in your config.

At the start of your config run:

require('lz.n').register_handler(require("handlers.load_before"))

In a spec:

  {
    -- If using nix it would be called "luasnip" instead of "LuaSnip"
    "LuaSnip",
    -- marking luasnip to "load_before" nvim-cmp, so that it is always available when nvim-cmp is loaded.
    load_before = { "nvim-cmp" },
    after = function (plugin)
      local luasnip = require 'luasnip'
      require('luasnip.loaders.from_vscode').lazy_load()
      luasnip.config.setup {}
    end,
  },

The handler:

local trigger_load = require("lz.n").trigger_load
local states = require("lz.n.handler.state").new()
---@type table<string, true>
local called = {}

---@type lz.n.Handler
---@diagnostic disable-next-line: missing-fields
local M = {
    spec_field = "load_before",
    lookup = states.lookup_plugin
}

---@param plugin lz.n.Plugin
function M.add(plugin)
    local dep_of = plugin.load_before

    ---@type string[]
    local needed_by = {}
    if type(dep_of) == "table" then
        ---@cast dep_of string[]
        needed_by = dep_of
    elseif type(dep_of) == "string" then
        needed_by = { dep_of }
    else
        return
    end

    for _, name in ipairs(needed_by) do
        if called[name] == true then
            trigger_load(plugin)
            return
        end
    end
    for _, name in ipairs(needed_by) do
        states.insert(name, plugin)
    end
end

---@param pname string
function M.del(pname)
    states.del(pname)
    called[pname] = true
    if states.has_pending_plugins(pname) then
        states.each_pending(pname,
            function (p)
                states.del(p.name)
                trigger_load(p)
            end
        )
    end
end

return M