mirror of
https://github.com/zoriya/lz.n.git
synced 2025-12-06 06:36:11 +00:00
feat: handler.state module
This commit is contained in:
@@ -403,6 +403,9 @@ require("lz.n").register_handler(handler)
|
||||
| lookup | `fun(name: string): lz.n.Plugin?` | lookup a plugin managed by this handler by name |
|
||||
<!-- 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
|
||||
@@ -440,11 +443,6 @@ The function provides two overloads, each suited for different use cases:
|
||||
Each handler has full authority over its internal state, ensuring it
|
||||
remains isolated and unaffected by external influences[^5],
|
||||
thereby preventing multiple sources of truth.
|
||||
- *Note:* If loading multiple plugins simultaneously,
|
||||
handlers should iterate over a deep copy (`:h vim.deepcopy`) of the plugins,
|
||||
verifying they are still pending before each `trigger_load` call.
|
||||
This practice allows for safe invocation of the stateful `trigger_load`
|
||||
in `before` and `after` hooks.
|
||||
2. **Stateful version:**
|
||||
- *Usage:* `trigger_load(plugin_name: string | string[], opts?: lz.n.lookup.Opts)`
|
||||
- *Returns:* A list of plugin names that were skipped
|
||||
|
||||
27
doc/lz.n.txt
27
doc/lz.n.txt
@@ -14,6 +14,7 @@ Table of Contents *lz.n.contents*
|
||||
········································································ |lz.n|
|
||||
lz.n Lua API ························································ |lz.n.api|
|
||||
lz.n type definitions ············································· |lz.n.types|
|
||||
Safe state management for handlers ························ |lz.n.handler.state|
|
||||
|
||||
==============================================================================
|
||||
lz.n Lua API *lz.n.api*
|
||||
@@ -223,4 +224,30 @@ lz.n.Handler *lz.n.Handler*
|
||||
Lookup a plugin by name.
|
||||
|
||||
|
||||
==============================================================================
|
||||
Safe state management for handlers *lz.n.handler.state*
|
||||
|
||||
This module is to be used by |lz.n.Handler| implementations.
|
||||
It provides an API for safely managing handler state,
|
||||
ensuring that `trigger_load` can be called in plugin hooks.
|
||||
|
||||
state.new() *state.new*
|
||||
|
||||
Returns: ~
|
||||
(lz.n.handler.State)
|
||||
|
||||
|
||||
lz.n.handler.State *lz.n.handler.State*
|
||||
|
||||
Fields: ~
|
||||
{insert} (fun(key:string,plugin:lz.n.Plugin))
|
||||
Insert a plugin by key.
|
||||
{del} (fun(plugin_name:string,callback?:fun(key:string)))
|
||||
Remove a plugin by its name.
|
||||
{has_pending_plugins} (fun(key:string):boolean)
|
||||
Check if there are pending plugins for a key
|
||||
{lookup_plugin} (fun(plugin_name:string):lz.n.Plugin)
|
||||
Lookup a plugin by its name.
|
||||
|
||||
|
||||
vim:tw=78:ts=8:noet:ft=help:norl:
|
||||
|
||||
@@ -2,8 +2,8 @@ local loader = require("lz.n.loader")
|
||||
|
||||
---@class lz.n.CmdHandler: lz.n.Handler
|
||||
|
||||
---@type table<string, table<string, lz.n.Plugin[]>>
|
||||
local pending = {}
|
||||
---@type lz.n.handler.State
|
||||
local state = require("lz.n.handler.state").new()
|
||||
|
||||
---@type lz.n.CmdHandler
|
||||
local M = {
|
||||
@@ -13,25 +13,14 @@ local M = {
|
||||
---@param name string
|
||||
---@return lz.n.Plugin?
|
||||
function M.lookup(name)
|
||||
return require("lz.n.handler.extra").lookup(pending, name)
|
||||
return state.lookup_plugin(name)
|
||||
end
|
||||
|
||||
---@param cmd string
|
||||
---@return string[] loaded_plugin_names
|
||||
local function load(cmd)
|
||||
vim.api.nvim_del_user_command(cmd)
|
||||
local plugins = pending[cmd]
|
||||
-- Make sure trigger_load calls in before hooks can't interfere with the state,
|
||||
-- but they can load a plugin before it's loaded by this handler
|
||||
vim
|
||||
.iter(vim.deepcopy(pending[cmd]))
|
||||
---@param plugin lz.n.Plugin
|
||||
:each(function(_, plugin)
|
||||
if pending[cmd][plugin.name] then
|
||||
loader.load(plugin)
|
||||
end
|
||||
end)
|
||||
return vim.tbl_keys(plugins)
|
||||
return state.each_pending(cmd, loader.load)
|
||||
end
|
||||
|
||||
---@param cmd string
|
||||
@@ -88,14 +77,9 @@ end
|
||||
|
||||
---@param name string
|
||||
function M.del(name)
|
||||
vim.iter(pending)
|
||||
:filter(function(_, plugins)
|
||||
return plugins[name] ~= nil
|
||||
end)
|
||||
:each(function(cmd, plugins)
|
||||
pcall(vim.api.nvim_del_user_command, cmd)
|
||||
plugins[name] = nil
|
||||
end)
|
||||
state.del(name, function(cmd)
|
||||
pcall(vim.api.nvim_del_user_command, cmd)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param plugin lz.n.Plugin
|
||||
@@ -105,8 +89,7 @@ function M.add(plugin)
|
||||
end
|
||||
---@param cmd string
|
||||
vim.iter(plugin.cmd):each(function(cmd)
|
||||
pending[cmd] = pending[cmd] or {}
|
||||
pending[cmd][plugin.name] = plugin
|
||||
state.insert(cmd, plugin)
|
||||
add_cmd(cmd)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -3,8 +3,8 @@ local loader = require("lz.n.loader")
|
||||
---@class lz.n.ColorschemeHandler: lz.n.Handler
|
||||
---@field augroup? integer
|
||||
|
||||
---@type table<string, table<string, lz.n.Plugin[]>>
|
||||
local pending = {}
|
||||
---@type lz.n.handler.State
|
||||
local state = require("lz.n.handler.state").new()
|
||||
|
||||
---@type lz.n.ColorschemeHandler
|
||||
local M = {
|
||||
@@ -15,33 +15,17 @@ local M = {
|
||||
---@param name string
|
||||
---@return lz.n.Plugin?
|
||||
function M.lookup(name)
|
||||
return require("lz.n.handler.extra").lookup(pending, name)
|
||||
return state.lookup_plugin(name)
|
||||
end
|
||||
|
||||
---@param name string
|
||||
function M.del(name)
|
||||
vim.iter(pending):each(function(_, plugins)
|
||||
plugins[name] = nil
|
||||
end)
|
||||
state.del(name)
|
||||
end
|
||||
|
||||
---@param name string
|
||||
local function on_colorscheme(name)
|
||||
local plugins = pending[name] or {}
|
||||
if vim.tbl_isempty(plugins) then
|
||||
-- already loaded
|
||||
return
|
||||
end
|
||||
-- Make sure trigger_load calls in before hooks can't interfere with the state,
|
||||
-- but they can load a plugin before it's loaded by this handler
|
||||
vim
|
||||
.iter(vim.deepcopy(pending[name]))
|
||||
---@param plugin lz.n.Plugin
|
||||
:each(function(_, plugin)
|
||||
if pending[name][plugin.name] then
|
||||
loader.load(plugin)
|
||||
end
|
||||
end)
|
||||
state.each_pending(name, loader.load)
|
||||
end
|
||||
|
||||
local function init()
|
||||
@@ -65,8 +49,7 @@ function M.add(plugin)
|
||||
init()
|
||||
---@param colorscheme string
|
||||
vim.iter(plugin.colorscheme):each(function(colorscheme)
|
||||
pending[colorscheme] = pending[colorscheme] or {}
|
||||
pending[colorscheme][plugin.name] = plugin
|
||||
state.insert(colorscheme, plugin)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ local lz_n_events = {
|
||||
|
||||
lz_n_events["User DeferredUIEnter"] = lz_n_events.DeferredUIEnter
|
||||
|
||||
---@type table<string, table<string, lz.n.Plugin[]>>
|
||||
local pending = {}
|
||||
---@type lz.n.handler.State
|
||||
local state = require("lz.n.handler.state").new()
|
||||
|
||||
---@type lz.n.EventHandler
|
||||
local M = {
|
||||
@@ -61,7 +61,7 @@ local M = {
|
||||
---@param name string
|
||||
---@return lz.n.Plugin?
|
||||
function M.lookup(name)
|
||||
return require("lz.n.handler.extra").lookup(pending, name)
|
||||
return state.lookup_plugin(name)
|
||||
end
|
||||
|
||||
-- Get all augroups for an event
|
||||
@@ -89,7 +89,7 @@ local event_triggers = {
|
||||
---@return lz.n.EventOpts[]
|
||||
local function get_state(event, buf, data)
|
||||
---@type lz.n.EventOpts[]
|
||||
local state = {}
|
||||
local st = {}
|
||||
while event do
|
||||
---@type lz.n.EventOpts
|
||||
local event_opts = {
|
||||
@@ -98,11 +98,11 @@ local function get_state(event, buf, data)
|
||||
buffer = buf,
|
||||
data = data,
|
||||
}
|
||||
table.insert(state, 1, event_opts)
|
||||
table.insert(st, 1, event_opts)
|
||||
data = nil -- only pass the data to the first event
|
||||
event = event_triggers[event]
|
||||
end
|
||||
return state
|
||||
return st
|
||||
end
|
||||
|
||||
-- Trigger an event
|
||||
@@ -152,24 +152,15 @@ local function add_event(event)
|
||||
once = true,
|
||||
pattern = event.pattern,
|
||||
callback = function(ev)
|
||||
if done or not pending[event.id] then
|
||||
if done or not state.has_pending_plugins(event.id) then
|
||||
return
|
||||
end
|
||||
-- HACK: work-around for https://github.com/neovim/neovim/issues/25526
|
||||
done = true
|
||||
local state = get_state(ev.event, ev.buf, ev.data)
|
||||
-- Make sure trigger_load calls in before hooks can't interfere with the state,
|
||||
-- but they can load a plugin before it's loaded by this handler
|
||||
vim
|
||||
.iter(vim.deepcopy(pending[event.id]))
|
||||
---@param plugin lz.n.Plugin
|
||||
:each(function(_, plugin)
|
||||
if pending[event.id][plugin.name] then
|
||||
loader.load(plugin)
|
||||
end
|
||||
end)
|
||||
local st = get_state(ev.event, ev.buf, ev.data)
|
||||
state.each_pending(event.id, loader.load)
|
||||
---@param s lz.n.EventOpts
|
||||
vim.iter(state):each(function(s)
|
||||
vim.iter(st):each(function(s)
|
||||
trigger(s)
|
||||
end)
|
||||
end,
|
||||
@@ -180,17 +171,14 @@ end
|
||||
function M.add(plugin)
|
||||
---@param event lz.n.Event
|
||||
vim.iter(plugin.event or {}):each(function(event)
|
||||
pending[event.id] = pending[event.id] or {}
|
||||
pending[event.id][plugin.name] = plugin
|
||||
state.insert(event.id, plugin)
|
||||
add_event(event)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param name string
|
||||
function M.del(name)
|
||||
vim.iter(pending):each(function(_, plugins)
|
||||
plugins[name] = nil
|
||||
end)
|
||||
state.del(name)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
---@mod lz.n.handler.extra Helper functions for use by handlers
|
||||
|
||||
local M = {}
|
||||
|
||||
---Look up a plugin in a plugin table, commonly used by handlers to
|
||||
---keep track of plugins they manage
|
||||
---@param plugin_tbl table<unknown, table<string, lz.n.Plugin>>
|
||||
---@param name string
|
||||
---@return lz.n.Plugin?
|
||||
function M.lookup(plugin_tbl, name)
|
||||
return vim
|
||||
.iter(plugin_tbl)
|
||||
---@param plugins table<string, lz.n.Plugin>
|
||||
:map(function(_, plugins)
|
||||
return plugins[name]
|
||||
end)
|
||||
---@param plugin lz.n.Plugin?
|
||||
:find(function(plugin)
|
||||
return plugin ~= nil
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -23,8 +23,8 @@ local function parse(value, mode)
|
||||
return ret
|
||||
end
|
||||
|
||||
---@type table<string, table<string, lz.n.Plugin[]>>
|
||||
local pending = {}
|
||||
---@type lz.n.handler.State
|
||||
local state = require("lz.n.handler.state").new()
|
||||
|
||||
---@type lz.n.KeysHandler
|
||||
local M = {
|
||||
@@ -48,7 +48,7 @@ local M = {
|
||||
---@param name string
|
||||
---@return lz.n.Plugin?
|
||||
function M.lookup(name)
|
||||
return require("lz.n.handler.extra").lookup(pending, name)
|
||||
return state.lookup_plugin(name)
|
||||
end
|
||||
|
||||
local skip = { mode = true, id = true, ft = true, rhs = true, lhs = true }
|
||||
@@ -103,16 +103,7 @@ local function add_keys(keys)
|
||||
vim.keymap.set(keys.mode, lhs, function()
|
||||
-- always delete the mapping immediately to prevent recursive mappings
|
||||
del(keys)
|
||||
-- Make sure trigger_load calls in before hooks can't interfere with the state,
|
||||
-- but they can load a plugin before it's loaded by this handler
|
||||
vim
|
||||
.iter(vim.deepcopy(pending[keys.id]))
|
||||
---@param plugin lz.n.Plugin
|
||||
:each(function(_, plugin)
|
||||
if pending[keys.id][plugin.name] then
|
||||
loader.load(plugin)
|
||||
end
|
||||
end)
|
||||
state.each_pending(keys.id, loader.load)
|
||||
-- Create the real buffer-local mapping
|
||||
if keys.ft then
|
||||
set(keys, buf)
|
||||
@@ -136,7 +127,7 @@ local function add_keys(keys)
|
||||
vim.api.nvim_create_autocmd("FileType", {
|
||||
pattern = keys.ft,
|
||||
callback = function(event)
|
||||
if pending[keys.id] then
|
||||
if state.has_pending_plugins[keys.id] then
|
||||
add(event.buf)
|
||||
else
|
||||
-- Only create the mapping if its managed by lz.n
|
||||
@@ -154,17 +145,14 @@ end
|
||||
function M.add(plugin)
|
||||
---@param key lz.n.Keys
|
||||
vim.iter(plugin.keys or {}):each(function(key)
|
||||
pending[key.id] = pending[key.id] or {}
|
||||
pending[key.id][plugin.name] = plugin
|
||||
state.insert(key.id, plugin)
|
||||
add_keys(key)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param name string
|
||||
function M.del(name)
|
||||
vim.iter(pending):each(function(_, plugins)
|
||||
plugins[name] = nil
|
||||
end)
|
||||
state.del(name)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
89
lua/lz/n/handler/state.lua
Normal file
89
lua/lz/n/handler/state.lua
Normal file
@@ -0,0 +1,89 @@
|
||||
---@mod lz.n.handler.state Safe state management for handlers
|
||||
---
|
||||
---@brief [[
|
||||
---This module is to be used by |lz.n.Handler| implementations.
|
||||
---It provides an API for safely managing handler state,
|
||||
---ensuring that `trigger_load` can be called in plugin hooks.
|
||||
---@brief ]]
|
||||
|
||||
local state = {}
|
||||
|
||||
---@return lz.n.handler.State
|
||||
function state.new()
|
||||
---@type table<string, table<string, lz.n.Plugin>>
|
||||
local pending = {}
|
||||
|
||||
---@type lz.n.handler.State
|
||||
return {
|
||||
insert = function(key, plugin)
|
||||
pending[key] = pending[key] or {}
|
||||
pending[key][plugin.name] = plugin
|
||||
end,
|
||||
|
||||
del = function(plugin_name, callback)
|
||||
vim.iter(pending)
|
||||
:filter(function(_, plugins)
|
||||
return plugins[plugin_name] ~= nil
|
||||
end)
|
||||
:each(
|
||||
---@param key string
|
||||
---@param plugins lz.n.Plugin[]
|
||||
function(key, plugins)
|
||||
if callback then
|
||||
callback(key)
|
||||
end
|
||||
plugins[plugin_name] = nil
|
||||
end
|
||||
)
|
||||
end,
|
||||
|
||||
has_pending_plugins = function(key)
|
||||
return pending[key] ~= nil and not vim.tbl_isempty(pending[key])
|
||||
end,
|
||||
|
||||
lookup_plugin = function(plugin_name)
|
||||
return vim
|
||||
.iter(pending)
|
||||
---@param plugins table<string, lz.n.Plugin>
|
||||
:map(function(_, plugins)
|
||||
return plugins[plugin_name]
|
||||
end)
|
||||
---@param plugin lz.n.Plugin?
|
||||
:find(function(plugin)
|
||||
return plugin ~= nil
|
||||
end)
|
||||
end,
|
||||
|
||||
each_pending = function(key, callback)
|
||||
local plugins = pending[key] or {}
|
||||
vim
|
||||
.iter(vim.deepcopy(plugins))
|
||||
---@param plugin lz.n.Plugin
|
||||
:each(function(_, plugin)
|
||||
if pending[key][plugin.name] then
|
||||
callback(plugin)
|
||||
end
|
||||
end)
|
||||
return vim.tbl_keys(plugins)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
---@class lz.n.handler.State
|
||||
---
|
||||
---Insert a plugin by key.
|
||||
---@field insert fun(key: string, plugin: lz.n.Plugin)
|
||||
---
|
||||
---Remove a plugin by its name.
|
||||
---@field del fun(plugin_name: string, callback?: fun(key: string))
|
||||
---
|
||||
---Check if there are pending plugins for a key
|
||||
---@field has_pending_plugins fun(key: string):boolean
|
||||
---
|
||||
---Lookup a plugin by its name.
|
||||
---@field lookup_plugin fun(plugin_name: string):lz.n.Plugin?
|
||||
---
|
||||
---Safely apply a callback to all pending plugins by key.
|
||||
---@field each_pending fun(key: string, callback: fun(plugin: lz.n.Plugin)): string[]
|
||||
|
||||
return state
|
||||
@@ -46,7 +46,7 @@
|
||||
];
|
||||
text = ''
|
||||
mkdir -p doc
|
||||
vimcats lua/lz/n/{init,meta}.lua > doc/lz.n.txt
|
||||
vimcats lua/lz/n/{init,meta,handler/state}.lua > doc/lz.n.txt
|
||||
'';
|
||||
};
|
||||
in {
|
||||
|
||||
Reference in New Issue
Block a user