diff --git a/README.md b/README.md index 8c6b550..f2368c8 100755 --- a/README.md +++ b/README.md @@ -395,13 +395,14 @@ require("lz.n").register_handler(handler) #### `lz.n.Handler` -| Property | Type | Description | -| --- | --- | --- | -| spec_field | `string` | the `lz.n.PluginSpec` field used to configure the handler | -| add | `fun(plugin: lz.n.Plugin)` | adds a plugin to the handler | -| del | `fun(name: string)` | removes a plugin from the handler by name | -| lookup | `fun(name: string): lz.n.Plugin?` | lookup a plugin managed by this handler by name | -| post_load | `fun()?` | ran once after each `require('lz.n').load` call, for handlers to create custom triggers such as the event handler's `DeferredUIEnter` event | +| Property | Type | Description | +| --- | --- | --- | +| spec_field | `string` | The `lz.n.PluginSpec` field used to configure the handler | +| parse | `fun(plugin: lz.n.Plugin, spec: unknown)?` | Parse a spec and add it to the passed in plugin | +| add | `fun(plugin: lz.n.Plugin)` | Adds a plugin to the handler | +| del | `fun(name: string)` | Removes a plugin from the handler by name | +| lookup | `fun(name: string): lz.n.Plugin?` | Lookup a plugin managed by this handler by name | +| post_load | `fun()?` | Ran once after each `require('lz.n').load` call, for handlers to create custom triggers such as the event handler's `DeferredUIEnter` event | To manage handler state safely, ensuring `trigger_load` can be invoked from diff --git a/lua/lz/n/handler/cmd.lua b/lua/lz/n/handler/cmd.lua index 74c3f7e..1ef355b 100644 --- a/lua/lz/n/handler/cmd.lua +++ b/lua/lz/n/handler/cmd.lua @@ -8,6 +8,20 @@ local state = require("lz.n.handler.state").new() ---@type lz.n.CmdHandler local M = { spec_field = "cmd", + ---@param cmd_spec? string[]|string + parse = function(result, cmd_spec) + if cmd_spec then + result.cmd = {} + end + if type(cmd_spec) == "string" then + table.insert(result.cmd, cmd_spec) + elseif type(cmd_spec) == "table" then + ---@param cmd_spec_ string + vim.iter(cmd_spec):each(function(cmd_spec_) + table.insert(result.cmd, cmd_spec_) + end) + end + end, } ---@param name string @@ -20,6 +34,7 @@ end ---@return string[] loaded_plugin_names local function load(cmd) vim.api.nvim_del_user_command(cmd) + ---@diagnostic disable-next-line: return-type-mismatch return state.each_pending(cmd, loader.load) end diff --git a/lua/lz/n/handler/colorscheme.lua b/lua/lz/n/handler/colorscheme.lua index 26ebe88..dd5f428 100644 --- a/lua/lz/n/handler/colorscheme.lua +++ b/lua/lz/n/handler/colorscheme.lua @@ -10,6 +10,20 @@ local state = require("lz.n.handler.state").new() local M = { augroup = nil, spec_field = "colorscheme", + ---@param colorscheme_spec? string[]|string + parse = function(plugin, colorscheme_spec) + if colorscheme_spec then + plugin.colorscheme = {} + end + if type(colorscheme_spec) == "string" then + table.insert(plugin.colorscheme, colorscheme_spec) + elseif type(colorscheme_spec) == "table" then + ---@param colorscheme_spec_ string + vim.iter(colorscheme_spec):each(function(colorscheme_spec_) + table.insert(plugin.colorscheme, colorscheme_spec_) + end) + end + end, } ---@param name string diff --git a/lua/lz/n/handler/event.lua b/lua/lz/n/handler/event.lua index be625bc..a0ffd60 100644 --- a/lua/lz/n/handler/event.lua +++ b/lua/lz/n/handler/event.lua @@ -10,7 +10,6 @@ local loader = require("lz.n.loader") ---@class lz.n.EventHandler: lz.n.Handler ---@field events table ---@field group number ----@field parse fun(spec: lz.n.EventSpec): lz.n.Event local lz_n_events = { DeferredUIEnter = { id = "DeferredUIEnter", event = "User", pattern = "DeferredUIEnter" }, @@ -21,40 +20,57 @@ lz_n_events["User DeferredUIEnter"] = lz_n_events.DeferredUIEnter ---@type lz.n.handler.State local state = require("lz.n.handler.state").new() +---@param spec lz.n.EventSpec +local function parse(spec) + local ret = lz_n_events[spec] + if ret then + return ret + end + if type(spec) == "string" then + local event, pattern = spec:match("^(%w+)%s+(.*)$") + event = event or spec + return { id = spec, event = event, pattern = pattern } + elseif vim.islist(spec) then + ret = { id = table.concat(spec, "|"), event = spec } + else + ret = spec --[[@as lz.n.Event]] + if not ret.id then + ---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch + ret.id = type(ret.event) == "string" and ret.event or table.concat(ret.event, "|") + if ret.pattern then + ---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch + ret.id = ret.id + .. " " + .. ( + type(ret.pattern) == "string" and ret.pattern + or table.concat(ret.pattern --[[@as table]], ", ") + ) + end + end + end + return ret +end + ---@type lz.n.EventHandler local M = { events = {}, group = vim.api.nvim_create_augroup("lz_n_handler_event", { clear = true }), spec_field = "event", - ---@param spec lz.n.EventSpec - parse = function(spec) - local ret = lz_n_events[spec] - if ret then - return ret + ---@param event_spec? lz.n.EventSpec + parse = function(plugin, event_spec) + if event_spec then + plugin.event = {} end - if type(spec) == "string" then - local event, pattern = spec:match("^(%w+)%s+(.*)$") - event = event or spec - return { id = spec, event = event, pattern = pattern } - elseif vim.islist(spec) then - ret = { id = table.concat(spec, "|"), event = spec } - else - ret = spec --[[@as lz.n.Event]] - if not ret.id then - ---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch - ret.id = type(ret.event) == "string" and ret.event or table.concat(ret.event, "|") - if ret.pattern then - ---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch - ret.id = ret.id - .. " " - .. ( - type(ret.pattern) == "string" and ret.pattern - or table.concat(ret.pattern --[[@as table]], ", ") - ) - end - end + if type(event_spec) == "string" or type(event_spec) == "table" and (event_spec.event or event_spec.pattern) then + local event = parse(event_spec) + table.insert(plugin.event, event) + elseif type(event_spec) == "table" then + ---@param ev lz.n.EventSpec[] + vim.iter(event_spec):each(function(ev) + local event = parse(ev) + table.insert(plugin.event, event) + end) end - return ret end, } diff --git a/lua/lz/n/handler/ft.lua b/lua/lz/n/handler/ft.lua index a6978c2..e5da142 100644 --- a/lua/lz/n/handler/ft.lua +++ b/lua/lz/n/handler/ft.lua @@ -1,21 +1,39 @@ local event = require("lz.n.handler.event") ---@class lz.n.FtHandler: lz.n.Handler ----@field parse fun(spec: lz.n.EventSpec): lz.n.Event + +---@param value string +---@return lz.n.Event +local function parse(value) + return { + id = value, + event = "FileType", + pattern = value, + } +end ---@type lz.n.FtHandler local M = { spec_field = "ft", - ---@param value string - ---@return lz.n.Event - parse = function(value) - return { - id = value, - event = "FileType", - pattern = value, - } - end, lookup = event.lookup, + ---@param ft_spec? string[]|string + parse = function(plugin, ft_spec) + if ft_spec then + plugin.event = plugin.event or {} + ---@diagnostic disable-next-line: inject-field + plugin.ft = nil + end + if type(ft_spec) == "string" then + local ft = parse(ft_spec) + table.insert(plugin.event, ft) + elseif type(ft_spec) == "table" then + ---@param ft_spec_ string + vim.iter(ft_spec):each(function(ft_spec_) + local ft = parse(ft_spec_) + table.insert(plugin.event, ft) + end) + end + end, } ---@param plugin lz.n.Plugin diff --git a/lua/lz/n/handler/init.lua b/lua/lz/n/handler/init.lua index 0214885..8707087 100644 --- a/lua/lz/n/handler/init.lua +++ b/lua/lz/n/handler/init.lua @@ -40,15 +40,6 @@ function M.lookup(name, opts) return result and vim.deepcopy(result) end ----@param spec lz.n.PluginSpec ----@return boolean -function M.is_lazy(spec) - ---@diagnostic disable-next-line: undefined-field - return spec.lazy or vim.iter(handlers):any(function(spec_field, _) - return spec[spec_field] ~= nil - end) -end - ---@param handler lz.n.Handler ---@return boolean success function M.register_handler(handler) @@ -97,6 +88,31 @@ local function enable(plugin) end) end +---@param spec lz.n.PluginSpec +---@return boolean +local function is_lazy(spec) + ---@diagnostic disable-next-line: undefined-field + return spec.lazy or vim.iter(handlers):any(function(spec_field, _) + return spec[spec_field] ~= nil + end) +end + +---Mutates the `plugin`. +---@param plugin lz.n.Plugin +---@param spec lz.n.PluginSpec +function M.parse(plugin, spec) + vim + .iter(handlers) + ---@param spec_field string + ---@param handler lz.n.Handler + :each(function(spec_field, handler) + if handler.parse then + handler.parse(plugin, spec[spec_field]) + end + end) + plugin.lazy = is_lazy(spec) +end + ---@param name string function M.disable(name) ---@param handler lz.n.Handler diff --git a/lua/lz/n/handler/keys.lua b/lua/lz/n/handler/keys.lua index 67301c0..da56c8a 100644 --- a/lua/lz/n/handler/keys.lua +++ b/lua/lz/n/handler/keys.lua @@ -5,7 +5,7 @@ local loader = require("lz.n.loader") ---@param value lz.n.KeysSpec ---@param mode? string ---@return lz.n.Keys -local function parse(value, mode) +local function parse_keys_spec(value, mode) local ret = vim.deepcopy(value) --[[@as lz.n.Keys]] ret.lhs = ret[1] or "" ret.rhs = ret[2] @@ -23,25 +23,42 @@ local function parse(value, mode) return ret end +---@param value string|lz.n.KeysSpec +---@return lz.n.Keys[] +local function parse(value) + value = type(value) == "string" and { value } or value --[[@as lz.n.KeysSpec]] + local modes = type(value.mode) == "string" and { value.mode } or value.mode --[[ @as string[] | nil ]] + if not modes then + return { parse_keys_spec(value) } + end + return vim.iter(modes) + :map(function(mode) + return parse_keys_spec(value, mode) + end) + :totable() +end + ---@type lz.n.handler.State local state = require("lz.n.handler.state").new() ---@type lz.n.KeysHandler local M = { spec_field = "keys", - ---@param value string|lz.n.KeysSpec - ---@return lz.n.Keys[] - parse = function(value) - value = type(value) == "string" and { value } or value --[[@as lz.n.KeysSpec]] - local modes = type(value.mode) == "string" and { value.mode } or value.mode --[[ @as string[] | nil ]] - if not modes then - return { parse(value) } + ---@param keys_spec? string|string[]|lz.n.KeysSpec[] + parse = function(plugin, keys_spec) + if keys_spec then + plugin.keys = {} end - return vim.iter(modes) - :map(function(mode) - return parse(value, mode) + if type(keys_spec) == "string" then + local keys = parse(keys_spec) + vim.list_extend(plugin.keys, keys) + elseif type(keys_spec) == "table" then + ---@param keys_spec_ string | lz.n.KeysSpec + vim.iter(keys_spec):each(function(keys_spec_) + local keys = parse(keys_spec_) + vim.list_extend(plugin.keys, keys) end) - :totable() + end end, } diff --git a/lua/lz/n/meta.lua b/lua/lz/n/meta.lua index 25e53f9..ea45c8b 100644 --- a/lua/lz/n/meta.lua +++ b/lua/lz/n/meta.lua @@ -103,10 +103,15 @@ --- Lookup a plugin by name. ---@field lookup fun(name: string): lz.n.Plugin? --- ----For setting up handler specific triggers such as ----the `DeferredUIEnter` event created by the builtin ----event handler. Called once after each call to require("lz.n").load +--- For setting up handler specific triggers such as +--- the `DeferredUIEnter` event created by the builtin +--- event handler. Called once after each call to require("lz.n").load ---@field post_load? fun() +--- +--- If a handler needs to transform the spec passed in by users +--- and add it to the plugin spec, this can be done here. +--- If unset, lz.n will add the `handler_spec` to the `lz.n.Plugin` as is. +---@field parse? fun(plugin: lz.n.Plugin, spec: unknown) ---@type lz.n.Config vim.g.lz_n = vim.g.lz_n diff --git a/lua/lz/n/spec.lua b/lua/lz/n/spec.lua index 97e3691..1db0655 100644 --- a/lua/lz/n/spec.lua +++ b/lua/lz/n/spec.lua @@ -79,75 +79,7 @@ local function parse(spec) local result = vim.deepcopy(spec) result.name = spec[1] result[1] = nil - local event_spec = spec.event - if event_spec then - result.event = {} - end - if type(event_spec) == "string" then - local event = require("lz.n.handler.event").parse(event_spec) - table.insert(result.event, event) - elseif type(event_spec) == "table" then - ---@param ev lz.n.EventSpec[] - vim.iter(event_spec):each(function(ev) - local event = require("lz.n.handler.event").parse(ev) - table.insert(result.event, event) - end) - end - local ft_spec = spec.ft - if ft_spec then - result.event = result.event or {} - ---@diagnostic disable-next-line: inject-field - result.ft = nil - end - if type(ft_spec) == "string" then - local ft = require("lz.n.handler.ft").parse(ft_spec) - table.insert(result.event, ft) - elseif type(ft_spec) == "table" then - ---@param ft_spec_ string - vim.iter(ft_spec):each(function(ft_spec_) - local ft = require("lz.n.handler.ft").parse(ft_spec_) - table.insert(result.event, ft) - end) - end - local keys_spec = spec.keys - if keys_spec then - result.keys = {} - end - if type(keys_spec) == "string" then - local keys = require("lz.n.handler.keys").parse(keys_spec) - vim.list_extend(result.keys, keys) - elseif type(keys_spec) == "table" then - ---@param keys_spec_ string | lz.n.KeysSpec - vim.iter(keys_spec):each(function(keys_spec_) - local keys = require("lz.n.handler.keys").parse(keys_spec_) - vim.list_extend(result.keys, keys) - end) - end - local cmd_spec = spec.cmd - if cmd_spec then - result.cmd = {} - end - if type(cmd_spec) == "string" then - table.insert(result.cmd, cmd_spec) - elseif type(cmd_spec) == "table" then - ---@param cmd_spec_ string - vim.iter(cmd_spec):each(function(cmd_spec_) - table.insert(result.cmd, cmd_spec_) - end) - end - local colorscheme_spec = spec.colorscheme - if colorscheme_spec then - result.colorscheme = {} - end - if type(colorscheme_spec) == "string" then - table.insert(result.colorscheme, colorscheme_spec) - elseif type(colorscheme_spec) == "table" then - ---@param colorscheme_spec_ string - vim.iter(colorscheme_spec):each(function(colorscheme_spec_) - table.insert(result.colorscheme, colorscheme_spec_) - end) - end - result.lazy = require("lz.n.handler").is_lazy(spec) + require("lz.n.handler").parse(result, spec) return result end diff --git a/spec/event_spec.lua b/spec/event_spec.lua index e8a5d3b..08a2b71 100644 --- a/spec/event_spec.lua +++ b/spec/event_spec.lua @@ -8,48 +8,49 @@ local spy = require("luassert.spy") describe("handlers.event", function() it("can parse from string", function() - assert.same({ + local plugin = {} + event.parse(plugin, "VimEnter") + assert.same({ { event = "VimEnter", id = "VimEnter", - }, event.parse("VimEnter")) + } }, plugin.event) end) it("can parse from table", function() - assert.same( - { - event = "VimEnter", - id = "VimEnter", - }, - event.parse({ - event = "VimEnter", - }) - ) - assert.same( + local plugin = {} + event.parse(plugin, { + event = "VimEnter", + }) + assert.same({ { + event = "VimEnter", + id = "VimEnter", + } }, plugin.event) + plugin = {} + event.parse(plugin, { event = { "VimEnter", "BufEnter" } }) + assert.same({ { event = { "VimEnter", "BufEnter" }, id = "VimEnter|BufEnter", }, - event.parse({ - event = { "VimEnter", "BufEnter" }, - }) - ) - assert.same( + }, plugin.event) + plugin = {} + event.parse(plugin, { + event = "BufEnter", + pattern = "*.lua", + }) + assert.same({ { event = "BufEnter", id = "BufEnter *.lua", pattern = "*.lua", }, - event.parse({ - event = "BufEnter", - pattern = "*.lua", - }) - ) + }, plugin.event) end) it("Event only loads plugin once", function() ---@type lz.n.Plugin local plugin = { name = "foo", - event = { event.parse("BufEnter") }, } + event.parse(plugin, "BufEnter") local spy_load = spy.on(loader, "_load") event.add(plugin) vim.api.nvim_exec_autocmds("BufEnter", {}) @@ -57,32 +58,33 @@ describe("handlers.event", function() assert.spy(spy_load).called(1) end) it("Multiple events only load plugin once", function() - ---@param events lz.n.Event[] - local function itt(events) + ---@param fst string + ---@param snd string + local function itt(fst, snd) ---@type lz.n.Plugin local plugin = { name = "foo", - event = events, } + event.parse(plugin, { fst, snd }) local spy_load = spy.on(loader, "_load") event.add(plugin) - vim.api.nvim_exec_autocmds(events[1].event, { + vim.api.nvim_exec_autocmds(plugin.event[1].event, { pattern = ".lua", }) - vim.api.nvim_exec_autocmds(events[2].event, { + vim.api.nvim_exec_autocmds(plugin.event[2].event, { pattern = ".lua", }) assert.spy(spy_load).called(1) end - itt({ event.parse("BufEnter"), event.parse("WinEnter") }) - itt({ event.parse("WinEnter"), event.parse("BufEnter") }) + itt("BufEnter", "WinEnter") + itt("WinEnter", "BufEnter") end) it("Plugins' event handlers are triggered", function() ---@type lz.n.Plugin local plugin = { name = "foo", - event = { event.parse("BufEnter") }, } + event.parse(plugin, "BufEnter") local triggered = false local orig_load = loader._load ---@diagnostic disable-next-line: duplicate-set-field @@ -104,8 +106,8 @@ describe("handlers.event", function() ---@type lz.n.Plugin local plugin = { name = "bla", - event = { event.parse("DeferredUIEnter") }, } + event.parse(plugin, "DeferredUIEnter") local spy_load = spy.on(loader, "_load") event.add(plugin) vim.api.nvim_exec_autocmds("User", { pattern = "DeferredUIEnter", modeline = false }) diff --git a/spec/ft_spec.lua b/spec/ft_spec.lua index df56c78..738ad9d 100644 --- a/spec/ft_spec.lua +++ b/spec/ft_spec.lua @@ -8,18 +8,22 @@ local spy = require("luassert.spy") describe("handlers.ft", function() it("can parse from string", function() + local plugin = {} + ft.parse(plugin, "rust") assert.same({ - event = "FileType", - id = "rust", - pattern = "rust", - }, ft.parse("rust")) + { + event = "FileType", + id = "rust", + pattern = "rust", + }, + }, plugin.event) end) it("filetype event loads plugins", function() ---@type lz.n.Plugin local plugin = { name = "Foo", - event = { ft.parse("rust") }, } + ft.parse(plugin, "rust") local spy_load = spy.on(loader, "_load") ft.add(plugin) vim.api.nvim_exec_autocmds("FileType", { pattern = "rust" }) diff --git a/spec/keys_spec.lua b/spec/keys_spec.lua index f0e05ef..8c5b1ab 100644 --- a/spec/keys_spec.lua +++ b/spec/keys_spec.lua @@ -15,9 +15,13 @@ describe("handlers.keys", function() } for _, test in ipairs(tests) do if test[3] then - assert.same(keys.parse(test[1])[1].id, keys.parse(test[2])[1].id) + local plguin = {} + keys.parse(plguin, { test[1], test[2] }) + assert.same(plguin.keys[1].id, plguin.keys[2].id) else - assert.is_not.same(keys.parse(test[1])[1].id, keys.parse(test[2])[1].id) + local plugin = {} + keys.parse(plugin, { test[1], test[2] }) + assert.is_not.same(plugin.keys[1].id, plugin.keys[2].id) end end end) @@ -26,8 +30,8 @@ describe("handlers.keys", function() ---@type lz.n.Plugin local plugin = { name = "foo", - keys = keys.parse(lhs), } + keys.parse(plugin, lhs) local spy_load = spy.on(loader, "_load") keys.add(plugin) local feed = vim.api.nvim_replace_termcodes("" .. lhs, true, true, true) @@ -37,31 +41,32 @@ describe("handlers.keys", function() -- end) it("Multiple keys only load plugin once", function() - ---@param lzkeys lz.n.Keys[] - local function itt(lzkeys) + ---@param fst string + ---@param snd string + local function itt(fst, snd) ---@type lz.n.Plugin local plugin = { name = "foo", - keys = lzkeys, } + keys.parse(plugin, { fst, snd }) local spy_load = spy.on(loader, "_load") keys.add(plugin) - local feed1 = vim.api.nvim_replace_termcodes("" .. lzkeys[1].lhs, true, true, true) + local feed1 = vim.api.nvim_replace_termcodes("" .. plugin.keys[1].lhs, true, true, true) vim.api.nvim_feedkeys(feed1, "ix", false) - local feed2 = vim.api.nvim_replace_termcodes("" .. lzkeys[2].lhs, true, true, true) + local feed2 = vim.api.nvim_replace_termcodes("" .. plugin.keys[2].lhs, true, true, true) vim.api.nvim_feedkeys(feed2, "ix", false) assert.spy(spy_load).called(1) end - itt({ keys.parse("tt")[1], keys.parse("ff")[1] }) - itt({ keys.parse("ff")[1], keys.parse("tt")[1] }) + itt("tt", "ff") + itt("ff", "tt") end) it("Plugins' keymaps are triggered", function() local lhs = "xy" ---@type lz.n.Plugin local plugin = { name = "baz", - keys = keys.parse(lhs), } + keys.parse(plugin, lhs) local triggered = false local orig_load = loader._load ---@diagnostic disable-next-line: duplicate-set-field diff --git a/spec/lz_n_spec.lua b/spec/lz_n_spec.lua index adee76d..c68c072 100644 --- a/spec/lz_n_spec.lua +++ b/spec/lz_n_spec.lua @@ -30,14 +30,12 @@ describe("lz.n", function() }) vim.api.nvim_exec_autocmds("FileType", { pattern = "toml" }) assert.spy(spy_load).called(2) - assert.spy(spy_load).called_with({ + local plugin = { name = "crates.nvim", lazy = true, - event = { - require("lz.n.handler.ft").parse("toml"), - require("lz.n.handler.ft").parse("rust"), - }, - }) + } + require("lz.n.handler.ft").parse(plugin, { "toml", "rust" }) + assert.spy(spy_load).called_with(plugin) vim.cmd.Telescope() assert.spy(spy_load).called(3) assert.spy(spy_load).called_with({