diff --git a/README.md b/README.md index 22dff38..80d54ec 100755 --- a/README.md +++ b/README.md @@ -315,7 +315,9 @@ Or ├── init.lua ``` -### Custom Handlers +## :electric_plug: API + +### Custom handlers You may register your own handlers to lazy-load plugins via other triggers not already covered by the plugin spec. @@ -335,25 +337,67 @@ require("lz.n").register_handler(handler) #### `lz.n.Handler` -| Property | Type | Description | -|----------|------|-------------| -| spec_field | `string` | the `lz.n.PluginSpec` field defined by the handler | -| add | `fun(plugin: lz.n.Plugin)` | adds a plugin to the handler | -| del | `fun(plugin: lz.n.Plugin)?` | removes a plugin from the 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 | -When writing custom handlers, -you can load the plugin and run the hooks from -the spec with the following function: +### Lua API + +The following Lua functions are part of the public API. + +> [!WARNING] +> +> If you use internal functions or modules that are not listed here, +> things may break without a major version bump. + +#### `trigger_load` + +You can manually load a plugin (and run the hooks from the spec) +with the following function: ```lua - ---@type fun(plugins: string | lz.n.Plugin | string[] | lz.n.Plugin[]) + ---@type fun(plugins: string | string[] | lz.n.Plugin | lz.n.Plugin[]) require('lz.n').trigger_load ``` -The function accepts plugin names or parsed plugin specs. -It will call the handler's `del` function (if it exists) after the `before` hooks, -and before `load` of the plugin's spec. +The function accepts plugin names (`string | string[]`, when called in another +plugin's hook), or `lz.n.Plugin` items (when called by a `lz.n.Handler`). +If called with a plugin name, it will use the registered +handlers' `lookup` functions to search for a plugin to load +(loading the first one it finds). +Once a plugin has been loaded, it will be removed from all handlers (via `del`). +As a result, calling `trigger_load` with a plugin name is idempotent. + +> [!IMPORTANT] +> +> This can be used to influence the order in which plugins are lazy-loaded, +> for example when a plugin depends on another one. +> +> However, we strongly recommend you consider alternatives before doing so. +> lz.n was designed with automatic dependency management in mind and hence +> **does not provide an API** to declare dependencies in the `lz.n.PluginSpec`. +> Plugin dependencies are usually just Lua libraries, which can be added to the +> `package.path` without any noticeable impact on startup time. +> +> If a plugin relies on another plugin's `plugin/` or `after/plugin/` scripts +> having been sourced before it is loaded, we consider this a bug, as this is not +> supported by Neovim's built-in loading mechanisms. +> +> Similarly, requiring users to worry about *the order in which they configure* +> plugins is bad design and defeats the purpose of automatic dependency management. + +#### `lookup` + +To lookup a plugin that is pending to be loaded by name, use: + +```lua + ---@type fun(name: string):lz.n.Plugin? + require('lz.n').lookup +``` ### Extensions diff --git a/lua/lz/n/handler/cmd.lua b/lua/lz/n/handler/cmd.lua index 86fe22d..292613d 100644 --- a/lua/lz/n/handler/cmd.lua +++ b/lua/lz/n/handler/cmd.lua @@ -4,10 +4,17 @@ local loader = require("lz.n.loader") ---@type lz.n.CmdHandler local M = { + ---@type table> pending = {}, spec_field = "cmd", } +---@param name string +---@return lz.n.Plugin? +function M.lookup(name) + return require("lz.n.handler.extra").lookup(M.pending, name) +end + ---@param cmd string local function load(cmd) vim.api.nvim_del_user_command(cmd) @@ -66,12 +73,16 @@ local function add_cmd(cmd) }) end ----@param plugin lz.n.Plugin -function M.del(plugin) - pcall(vim.api.nvim_del_user_command, plugin.cmd) - vim.iter(M.pending):each(function(_, plugins) - plugins[plugin.name] = nil - end) +---@param name string +function M.del(name) + vim.iter(M.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) end ---@param plugin lz.n.Plugin @@ -82,7 +93,7 @@ function M.add(plugin) ---@param cmd string vim.iter(plugin.cmd):each(function(cmd) M.pending[cmd] = M.pending[cmd] or {} - M.pending[cmd][plugin.name] = plugin.name + M.pending[cmd][plugin.name] = plugin add_cmd(cmd) end) end diff --git a/lua/lz/n/handler/colorscheme.lua b/lua/lz/n/handler/colorscheme.lua index af06b6e..5169822 100644 --- a/lua/lz/n/handler/colorscheme.lua +++ b/lua/lz/n/handler/colorscheme.lua @@ -5,15 +5,22 @@ local loader = require("lz.n.loader") ---@type lz.n.ColorschemeHandler local M = { + ---@type table> pending = {}, augroup = nil, spec_field = "colorscheme", } ----@param plugin lz.n.Plugin -function M.del(plugin) +---@param name string +---@return lz.n.Plugin? +function M.lookup(name) + return require("lz.n.handler.extra").lookup(M.pending, name) +end + +---@param name string +function M.del(name) vim.iter(M.pending):each(function(_, plugins) - plugins[plugin.name] = nil + plugins[name] = nil end) end @@ -49,7 +56,7 @@ function M.add(plugin) ---@param colorscheme string vim.iter(plugin.colorscheme):each(function(colorscheme) M.pending[colorscheme] = M.pending[colorscheme] or {} - M.pending[colorscheme][plugin.name] = plugin.name + M.pending[colorscheme][plugin.name] = plugin end) end diff --git a/lua/lz/n/handler/event.lua b/lua/lz/n/handler/event.lua index e43b270..adb6f03 100644 --- a/lua/lz/n/handler/event.lua +++ b/lua/lz/n/handler/event.lua @@ -20,6 +20,7 @@ lz_n_events["User DeferredUIEnter"] = lz_n_events.DeferredUIEnter ---@type lz.n.EventHandler local M = { + ---@type table> pending = {}, events = {}, group = vim.api.nvim_create_augroup("lz_n_handler_event", { clear = true }), @@ -56,6 +57,12 @@ local M = { end, } +---@param name string +---@return lz.n.Plugin? +function M.lookup(name) + return require("lz.n.handler.extra").lookup(M.pending, name) +end + -- Get all augroups for an event ---@param event string ---@return string[] @@ -166,15 +173,15 @@ function M.add(plugin) ---@param event lz.n.Event vim.iter(plugin.event or {}):each(function(event) M.pending[event.id] = M.pending[event.id] or {} - M.pending[event.id][plugin.name] = plugin.name + M.pending[event.id][plugin.name] = plugin add_event(event) end) end ----@param plugin lz.n.Plugin -function M.del(plugin) +---@param name string +function M.del(name) vim.iter(M.pending):each(function(_, plugins) - plugins[plugin.name] = nil + plugins[name] = nil end) end diff --git a/lua/lz/n/handler/extra.lua b/lua/lz/n/handler/extra.lua new file mode 100644 index 0000000..fe478a9 --- /dev/null +++ b/lua/lz/n/handler/extra.lua @@ -0,0 +1,23 @@ +---@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/ft.lua b/lua/lz/n/handler/ft.lua index 362d2bc..a6978c2 100644 --- a/lua/lz/n/handler/ft.lua +++ b/lua/lz/n/handler/ft.lua @@ -5,7 +5,6 @@ local event = require("lz.n.handler.event") ---@type lz.n.FtHandler local M = { - pending = {}, spec_field = "ft", ---@param value string ---@return lz.n.Event @@ -16,6 +15,7 @@ local M = { pattern = value, } end, + lookup = event.lookup, } ---@param plugin lz.n.Plugin @@ -23,9 +23,9 @@ function M.add(plugin) event.add(plugin) end ----@param plugin lz.n.Plugin -function M.del(plugin) - event.del(plugin) +---@param name string +function M.del(name) + event.del(name) end return M diff --git a/lua/lz/n/handler/init.lua b/lua/lz/n/handler/init.lua index 7a43649..8113080 100644 --- a/lua/lz/n/handler/init.lua +++ b/lua/lz/n/handler/init.lua @@ -8,6 +8,21 @@ local handlers = { colorscheme = require("lz.n.handler.colorscheme"), } +---@param name string +---@return lz.n.Plugin? +function M.lookup(name) + return vim + .iter(handlers) + ---@param handler lz.n.Handler + :map(function(_, handler) + return handler.lookup(name) + end) + ---@param result lz.n.Plugin? + :find(function(result) + return result ~= nil + end) +end + ---@param spec lz.n.PluginSpec ---@return boolean function M.is_lazy(spec) @@ -41,12 +56,11 @@ local function enable(plugin) end) end -function M.disable(plugin) +---@param name string +function M.disable(name) ---@param handler lz.n.Handler vim.iter(handlers):each(function(_, handler) - if handler.del then - handler.del(plugin) - end + handler.del(name) end) end diff --git a/lua/lz/n/handler/keys.lua b/lua/lz/n/handler/keys.lua index 244fde7..b99d525 100644 --- a/lua/lz/n/handler/keys.lua +++ b/lua/lz/n/handler/keys.lua @@ -25,6 +25,7 @@ end ---@type lz.n.KeysHandler local M = { + ---@type table> pending = {}, spec_field = "keys", ---@param value string|lz.n.KeysSpec @@ -43,6 +44,12 @@ local M = { end, } +---@param name string +---@return lz.n.Plugin? +function M.lookup(name) + return require("lz.n.handler.extra").lookup(M.pending, name) +end + local skip = { mode = true, id = true, ft = true, rhs = true, lhs = true } ---@param keys lz.n.Keys @@ -142,15 +149,15 @@ function M.add(plugin) ---@param key lz.n.Keys vim.iter(plugin.keys or {}):each(function(key) M.pending[key.id] = M.pending[key.id] or {} - M.pending[key.id][plugin.name] = plugin.name + M.pending[key.id][plugin.name] = plugin add_keys(key) end) end ----@param plugin lz.n.Plugin -function M.del(plugin) +---@param name string +function M.del(name) vim.iter(M.pending):each(function(_, plugins) - plugins[plugin.name] = nil + plugins[name] = nil end) end diff --git a/lua/lz/n/init.lua b/lua/lz/n/init.lua index 8b28d22..8e5afd9 100644 --- a/lua/lz/n/init.lua +++ b/lua/lz/n/init.lua @@ -15,10 +15,21 @@ local deferred_ui_enter = vim.schedule_wrap(function() end) ---@type fun(handler: lz.n.Handler): boolean -M.register_handler = require("lz.n.handler").register_handler +M.register_handler = function(...) + return require("lz.n.handler").register_handler(...) +end ----@type fun(plugins: string | lz.n.Plugin | string[] | lz.n.Plugin[]) -M.trigger_load = require("lz.n.loader").load +--- Accepts plugin names (`string | string[]`, when called in another +--- plugin's hook), or |lz.n.Plugin| items (when called by a |lz.n.Handler|). +--- If called with a plugin name, it will use the registered +--- handlers' `lookup` functions to search for a plugin to load +--- (loading the first one it finds). +--- Once a plugin has been loaded, it will be removed from all handlers (via `del`). +--- As a result, calling `trigger_load` with a plugin name is idempotent. +---@param plugins string | lz.n.Plugin | string[] | lz.n.Plugin[] +M.trigger_load = function(plugins) + require("lz.n.loader").load(plugins, M.lookup) +end ---@overload fun(spec: lz.n.Spec) ---@overload fun(import: string) @@ -28,42 +39,17 @@ function M.load(spec) end --- @cast spec lz.n.Spec local spec_mod = require("lz.n.spec") - local is_single_plugin_spec = spec_mod.is_single_plugin_spec(spec) local plugins = spec_mod.parse(spec) - -- add to state before loading anything, to prevent multiple loads being called - -- from within other eager plugin specs - local state = require("lz.n.state") - if is_single_plugin_spec then - local ok, updated_plugins = pcall(vim.tbl_deep_extend, "error", state.plugins, plugins) - if not ok then - return vim.schedule(function() - vim.notify("Cannot load the same plugin specs more than once", vim.log.levels.ERROR, { title = "lz.n" }) - end) - end - state.plugins = updated_plugins - else - if state.plugins[spec[1]] then - return vim.schedule(function() - vim.notify( - ("Plugin %s has already been registered for lazy loading"):format(spec[1]), - vim.log.levels.ERROR, - { title = "lz.n" } - ) - end) - end - state.plugins = plugins - end - -- calls handler add functions require("lz.n.handler").init(plugins) - -- because this calls the handler's del functions, - -- this should be ran after the handlers are given the plugin. - -- even if the plugin isnt supposed to have been added to any of them + -- Because this calls the handlers' `del` functions, + -- this should be ran after the plugins are registered with the handlers. + -- even if an eager plugin isn't supposed to have been added to any of them + -- This allows even startup plugins to call + -- `require('lz.n').trigger_load()` safely require("lz.n.loader").load_startup_plugins(plugins) - -- in addition, this allows even startup plugins to call - -- require('lz.n').trigger_load('someplugin') safely if vim.v.vim_did_enter == 1 then deferred_ui_enter() @@ -76,4 +62,11 @@ function M.load(spec) end end +---Lookup a plugin that is pending to be loaded by name. +---@param name string +---@return lz.n.Plugin? +function M.lookup(name) + return require("lz.n.handler").lookup(name) +end + return M diff --git a/lua/lz/n/loader.lua b/lua/lz/n/loader.lua index 04e3e36..81c6c48 100644 --- a/lua/lz/n/loader.lua +++ b/lua/lz/n/loader.lua @@ -1,7 +1,5 @@ ---@mod lz.n.loader -local state = require("lz.n.state") - local M = {} local DEFAULT_PRIORITY = 50 @@ -14,7 +12,7 @@ function M._load(plugin) if plugin.enabled == false or (type(plugin.enabled) == "function" and not plugin.enabled()) then return end - require("lz.n.handler").disable(plugin) + require("lz.n.handler").disable(plugin.name) ---@type fun(name: string) | nil local load_impl = plugin.load or vim.tbl_get(vim.g, "lz_n", "load") if type(load_impl) == "function" then @@ -101,7 +99,8 @@ local function hook(hook_key, plugin) end ---@param plugins string | lz.n.Plugin | string[] | lz.n.Plugin[] -function M.load(plugins) +---@param lookup? fun(name: string): lz.n.Plugin? +function M.load(plugins, lookup) plugins = (type(plugins) == "string" or plugins.name) and { plugins } or plugins ---@cast plugins (string|lz.n.Plugin)[] for _, plugin in pairs(plugins) do @@ -109,9 +108,8 @@ function M.load(plugins) -- https://github.com/nvim-neorocks/lz.n/pull/21 local loadable = true if type(plugin) == "string" then - if state.plugins[plugin] then - plugin = state.plugins[plugin] - else + plugin = lookup and lookup(plugin) or plugin + if type(plugin) == "string" then vim.notify("Plugin " .. plugin .. " not found", vim.log.levels.ERROR, { title = "lz.n" }) loadable = false end diff --git a/lua/lz/n/meta.lua b/lua/lz/n/meta.lua index 1295e87..9703f57 100644 --- a/lua/lz/n/meta.lua +++ b/lua/lz/n/meta.lua @@ -89,9 +89,18 @@ error("Cannot import a meta module") --- @field load? fun(name: string) --- @class lz.n.Handler +--- +--- The |lz.n.PluginSpec| field used to configure this handler. --- @field spec_field string +--- +--- Add a plugin to this handler. --- @field add fun(plugin: lz.n.Plugin) ---- @field del? fun(plugin: lz.n.Plugin) +--- +--- Remove a plugin from this handler by name. +--- @field del fun(name: string) +--- +--- Lookup a plugin by name. +--- @field lookup fun(name: string): lz.n.Plugin? --- @type lz.n.Config vim.g.lz_n = vim.g.lz_n diff --git a/lua/lz/n/state.lua b/lua/lz/n/state.lua deleted file mode 100644 index e83cc46..0000000 --- a/lua/lz/n/state.lua +++ /dev/null @@ -1,8 +0,0 @@ ----@mod lz.n.state - -local M = {} - ----@type table -M.plugins = {} - -return M diff --git a/spec/cmd_spec.lua b/spec/cmd_spec.lua index fc596ff..63f6217 100644 --- a/spec/cmd_spec.lua +++ b/spec/cmd_spec.lua @@ -3,7 +3,6 @@ vim.g.lz_n = { load = function() end, } local cmd = require("lz.n.handler.cmd") -local state = require("lz.n.state") local loader = require("lz.n.loader") local spy = require("luassert.spy") @@ -15,7 +14,6 @@ describe("handlers.cmd", function() cmd = { "Foo" }, } local spy_load = spy.on(loader, "_load") - state.plugins[plugin.name] = plugin cmd.add(plugin) assert.is_not_nil(vim.cmd.Foo) local counter = 0 @@ -49,7 +47,6 @@ describe("handlers.cmd", function() cmd = commands, } local spy_load = spy.on(loader, "_load") - state.plugins[plugin.name] = plugin cmd.add(plugin) vim.cmd[commands[1]]() vim.cmd[commands[2]]() diff --git a/spec/colorscheme_spec.lua b/spec/colorscheme_spec.lua index 3c86110..e5f7ca5 100644 --- a/spec/colorscheme_spec.lua +++ b/spec/colorscheme_spec.lua @@ -2,7 +2,6 @@ vim.g.lz_n = { load = function() end, } local colorscheme = require("lz.n.handler.colorscheme") -local state = require("lz.n.state") local loader = require("lz.n.loader") local spy = require("luassert.spy") @@ -14,7 +13,6 @@ describe("handlers.colorscheme", function() colorscheme = { "sweetie" }, } local spy_load = spy.on(loader, "_load") - state.plugins[plugin.name] = plugin colorscheme.add(plugin) pcall(vim.cmd.colorscheme, "sweetie") pcall(vim.cmd.colorscheme, "sweetie") diff --git a/spec/event_spec.lua b/spec/event_spec.lua index 688a53b..e8a5d3b 100644 --- a/spec/event_spec.lua +++ b/spec/event_spec.lua @@ -3,7 +3,6 @@ vim.g.lz_n = { load = function() end, } local event = require("lz.n.handler.event") -local state = require("lz.n.state") local loader = require("lz.n.loader") local spy = require("luassert.spy") @@ -52,7 +51,6 @@ describe("handlers.event", function() event = { event.parse("BufEnter") }, } local spy_load = spy.on(loader, "_load") - state.plugins[plugin.name] = plugin event.add(plugin) vim.api.nvim_exec_autocmds("BufEnter", {}) vim.api.nvim_exec_autocmds("BufEnter", {}) @@ -67,7 +65,6 @@ describe("handlers.event", function() event = events, } local spy_load = spy.on(loader, "_load") - state.plugins[plugin.name] = plugin event.add(plugin) vim.api.nvim_exec_autocmds(events[1].event, { pattern = ".lua", @@ -98,7 +95,6 @@ describe("handlers.event", function() group = vim.api.nvim_create_augroup("foo", {}), }) end - state.plugins[plugin.name] = plugin event.add(plugin) vim.api.nvim_exec_autocmds("BufEnter", {}) assert.True(triggered) @@ -111,7 +107,6 @@ describe("handlers.event", function() event = { event.parse("DeferredUIEnter") }, } local spy_load = spy.on(loader, "_load") - state.plugins[plugin.name] = plugin event.add(plugin) vim.api.nvim_exec_autocmds("User", { pattern = "DeferredUIEnter", modeline = false }) assert.spy(spy_load).called(1) diff --git a/spec/ft_spec.lua b/spec/ft_spec.lua index 75a76f6..df56c78 100644 --- a/spec/ft_spec.lua +++ b/spec/ft_spec.lua @@ -3,7 +3,6 @@ vim.g.lz_n = { load = function() end, } local ft = require("lz.n.handler.ft") -local state = require("lz.n.state") local loader = require("lz.n.loader") local spy = require("luassert.spy") @@ -22,7 +21,6 @@ describe("handlers.ft", function() event = { ft.parse("rust") }, } local spy_load = spy.on(loader, "_load") - state.plugins[plugin.name] = plugin ft.add(plugin) vim.api.nvim_exec_autocmds("FileType", { pattern = "rust" }) vim.api.nvim_exec_autocmds("FileType", { pattern = "rust" }) diff --git a/spec/keys_spec.lua b/spec/keys_spec.lua index ad59456..f0e05ef 100644 --- a/spec/keys_spec.lua +++ b/spec/keys_spec.lua @@ -3,7 +3,6 @@ vim.g.lz_n = { load = function() end, } local keys = require("lz.n.handler.keys") -local state = require("lz.n.state") local loader = require("lz.n.loader") local spy = require("luassert.spy") @@ -30,7 +29,6 @@ describe("handlers.keys", function() keys = keys.parse(lhs), } local spy_load = spy.on(loader, "_load") - state.plugins[plugin.name] = plugin keys.add(plugin) local feed = vim.api.nvim_replace_termcodes("" .. lhs, true, true, true) vim.api.nvim_feedkeys(feed, "ix", false) @@ -47,7 +45,6 @@ describe("handlers.keys", function() keys = lzkeys, } local spy_load = spy.on(loader, "_load") - state.plugins[plugin.name] = plugin keys.add(plugin) local feed1 = vim.api.nvim_replace_termcodes("" .. lzkeys[1].lhs, true, true, true) vim.api.nvim_feedkeys(feed1, "ix", false) @@ -74,7 +71,6 @@ describe("handlers.keys", function() end) orig_load(...) end - state.plugins[plugin.name] = plugin keys.add(plugin) local feed = vim.api.nvim_replace_termcodes("" .. lhs, true, true, true) vim.api.nvim_feedkeys(feed, "ix", false) diff --git a/spec/register_handler_spec.lua b/spec/register_handler_spec.lua index f56686e..12515a6 100644 --- a/spec/register_handler_spec.lua +++ b/spec/register_handler_spec.lua @@ -5,16 +5,35 @@ vim.g.lz_n = { local lz_n = require("lz.n") local spy = require("luassert.spy") +---@type lz.n.Plugin +local testplugin = { + name = "testplugin", + testfield = { "a", "b" }, + lazy = true, +} + describe("handlers.custom", function() ---@class TestHandler: lz.n.Handler + local mock_state = {} ---@type TestHandler - local hndl = { + local mock_hndl = { spec_field = "testfield", - add = function(_) end, - del = function(_) end, + add = function(plugin) + mock_state[plugin.name] = plugin + end, + ---@param name string + del = function(name) + mock_state[name] = nil + end, + ---@param name string + ---@return lz.n.Plugin? + lookup = function(name) + return mock_state[name] + end, } - local addspy = spy.on(hndl, "add") - local delspy = spy.on(hndl, "del") + + local addspy = spy.on(mock_hndl, "add") + local delspy = spy.on(mock_hndl, "del") it("Duplicate handlers fail to register", function() local notispy = spy.new(function() end) -- NOTE: teardown fails if you don't temporarily replace vim.notify @@ -25,23 +44,19 @@ describe("handlers.custom", function() vim.notify = og_notify end) it("can add plugins to the handler", function() - assert.True(lz_n.register_handler(hndl)) + assert.True(lz_n.register_handler(mock_hndl)) lz_n.load({ "testplugin", testfield = { "a", "b" }, }) - assert.spy(addspy).called_with({ - name = "testplugin", - testfield = { "a", "b" }, - lazy = true, - }) + assert.spy(addspy).called_with(testplugin) end) it("loading a plugin removes it from the handler", function() - lz_n.trigger_load("testplugin") - assert.spy(delspy).called_with({ - name = "testplugin", - testfield = { "a", "b" }, - lazy = true, - }) + lz_n.trigger_load(testplugin.name) + assert.spy(delspy).called_with(testplugin.name) + end) + it("trigger_load is idempotent when called with a plugin name", function() + lz_n.trigger_load(testplugin.name) + assert.spy(delspy).called(1) end) end)