feat: handler.state module

This commit is contained in:
Marc Jakobi
2024-08-28 14:00:38 +02:00
committed by Marc Jakobi
parent 8611566831
commit efe92fa725
9 changed files with 153 additions and 120 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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 {