From efe92fa725afcd8ac02b97866f7f41cba0d21a4f Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Wed, 28 Aug 2024 14:00:38 +0200 Subject: [PATCH] feat: handler.state module --- README.md | 8 ++- doc/lz.n.txt | 27 ++++++++++ lua/lz/n/handler/cmd.lua | 33 +++--------- lua/lz/n/handler/colorscheme.lua | 29 +++-------- lua/lz/n/handler/event.lua | 36 +++++-------- lua/lz/n/handler/extra.lua | 23 --------- lua/lz/n/handler/keys.lua | 26 +++------- lua/lz/n/handler/state.lua | 89 ++++++++++++++++++++++++++++++++ nix/ci-overlay.nix | 2 +- 9 files changed, 153 insertions(+), 120 deletions(-) delete mode 100644 lua/lz/n/handler/extra.lua create mode 100644 lua/lz/n/handler/state.lua diff --git a/README.md b/README.md index 638569b..5a6d24c 100755 --- a/README.md +++ b/README.md @@ -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 | +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 diff --git a/doc/lz.n.txt b/doc/lz.n.txt index 55854dd..d10b64c 100644 --- a/doc/lz.n.txt +++ b/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: diff --git a/lua/lz/n/handler/cmd.lua b/lua/lz/n/handler/cmd.lua index 2708f09..74c3f7e 100644 --- a/lua/lz/n/handler/cmd.lua +++ b/lua/lz/n/handler/cmd.lua @@ -2,8 +2,8 @@ local loader = require("lz.n.loader") ---@class lz.n.CmdHandler: lz.n.Handler ----@type table> -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 diff --git a/lua/lz/n/handler/colorscheme.lua b/lua/lz/n/handler/colorscheme.lua index 3d1c79f..79f86f8 100644 --- a/lua/lz/n/handler/colorscheme.lua +++ b/lua/lz/n/handler/colorscheme.lua @@ -3,8 +3,8 @@ local loader = require("lz.n.loader") ---@class lz.n.ColorschemeHandler: lz.n.Handler ---@field augroup? integer ----@type table> -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 diff --git a/lua/lz/n/handler/event.lua b/lua/lz/n/handler/event.lua index f4e6e7a..3e14146 100644 --- a/lua/lz/n/handler/event.lua +++ b/lua/lz/n/handler/event.lua @@ -18,8 +18,8 @@ local lz_n_events = { lz_n_events["User DeferredUIEnter"] = lz_n_events.DeferredUIEnter ----@type table> -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 diff --git a/lua/lz/n/handler/extra.lua b/lua/lz/n/handler/extra.lua deleted file mode 100644 index fe478a9..0000000 --- a/lua/lz/n/handler/extra.lua +++ /dev/null @@ -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> ----@param name string ----@return lz.n.Plugin? -function M.lookup(plugin_tbl, name) - return vim - .iter(plugin_tbl) - ---@param plugins table - :map(function(_, plugins) - return plugins[name] - end) - ---@param plugin lz.n.Plugin? - :find(function(plugin) - return plugin ~= nil - end) -end - -return M diff --git a/lua/lz/n/handler/keys.lua b/lua/lz/n/handler/keys.lua index df76876..51f3731 100644 --- a/lua/lz/n/handler/keys.lua +++ b/lua/lz/n/handler/keys.lua @@ -23,8 +23,8 @@ local function parse(value, mode) return ret end ----@type table> -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 diff --git a/lua/lz/n/handler/state.lua b/lua/lz/n/handler/state.lua new file mode 100644 index 0000000..298e4d7 --- /dev/null +++ b/lua/lz/n/handler/state.lua @@ -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> + 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 + :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 diff --git a/nix/ci-overlay.nix b/nix/ci-overlay.nix index 0e22307..abaa04f 100755 --- a/nix/ci-overlay.nix +++ b/nix/ci-overlay.nix @@ -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 {