From d197fc6281550f3872c10b1e3ae16b55d656b27b Mon Sep 17 00:00:00 2001 From: yavorski <3436517+yavorski@users.noreply.github.com> Date: Sun, 17 May 2026 18:43:34 +0300 Subject: [PATCH] refactor!: :ZPack with subcommands --- README.md | 19 +-- doc/zpack.txt | 66 +++++----- lua/zpack/api/init.lua | 2 +- lua/zpack/commands.lua | 251 +++++++++++++++++++++++++------------ lua/zpack/deprecation.lua | 12 ++ lua/zpack/init.lua | 14 ++- tests/cmd_name_test.lua | 128 +++++++++++++++++++ tests/deprecation_test.lua | 44 +++++++ tests/helpers.lua | 13 +- tests/public_api_test.lua | 6 +- tests/run_all.lua | 1 + tests/zclean_test.lua | 14 +-- tests/zdelete_test.lua | 26 ++-- tests/zload_test.lua | 26 ++-- tests/zrestore_test.lua | 2 +- tests/zupdate_test.lua | 2 +- 16 files changed, 455 insertions(+), 171 deletions(-) create mode 100644 tests/cmd_name_test.lua diff --git a/README.md b/README.md index cbaa91b..b7e8bb4 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,15 @@ return { #### Commands -zpack provides the following commands (default prefix: `Z`, customizable via `cmd_prefix` option): - -- `:ZUpdate [plugin]` - Update all plugins, or a specific plugin if provided (supports tab completion). See `:h vim.pack.update()` -- `:ZRestore [plugin]` - Restore all plugins, or a specific plugin, to the lockfile state (supports tab completion). Requires a lockfile to exist (created automatically by `:ZUpdate`). See `:h vim.pack.update()` -- `:ZClean` - Remove plugins that are no longer in your spec -- `:ZBuild[!] [plugin]` - Run build hook for a specific plugin, or all plugins with `!` (supports tab completion) -- `:ZLoad[!] [plugin]` - Load a specific unloaded plugin, or all unloaded plugins with `!` (supports tab completion) -- `:ZDelete[!] [plugin]` - Remove a specific plugin, or all plugins with `!` (supports tab completion) +zpack provides a single user command, `:ZPack`, with subcommands. The command +name is configurable via the `cmd_name` option (default: `ZPack`). + +- `:ZPack update [plugin]` - Update all plugins, or a specific plugin if provided (supports tab completion). See `:h vim.pack.update()` +- `:ZPack restore [plugin]` - Restore all plugins, or a specific plugin, to the lockfile state (supports tab completion). Requires a lockfile to exist (created automatically by `:ZPack update`). See `:h vim.pack.update()` +- `:ZPack clean` - Remove plugins that are no longer in your spec +- `:ZPack[!] build [plugin]` - Run build hook for a specific plugin, or all plugins with `!` (supports tab completion) +- `:ZPack[!] load [plugin]` - Load a specific unloaded plugin, or all unloaded plugins with `!` (supports tab completion) +- `:ZPack[!] delete [plugin]` - Remove a specific plugin, or all plugins with `!` (supports tab completion) - Deleting active plugins in your spec can result in errors in your current session. Restart Neovim to re-install them. @@ -89,7 +90,7 @@ require('zpack').setup({ performance = { vim_loader = true, -- enables vim.loader for faster startup (default: true) }, - cmd_prefix = 'Z', -- command prefix: :ZUpdate, :ZClean, etc. (default: 'Z') + cmd_name = 'ZPack', -- name of the user command (default: 'ZPack'). Use as: :ZPack update, :ZPack clean, etc. }) ``` diff --git a/doc/zpack.txt b/doc/zpack.txt index e57d1b8..0aa08ab 100644 --- a/doc/zpack.txt +++ b/doc/zpack.txt @@ -86,33 +86,35 @@ Each file should return a spec or list of specs (see |zpack-examples| or ------------------------------------------------------------------------------ 4.2 COMMANDS *zpack-commands* -zpack provides the following commands (default prefix: `Z`, customizable via -|zpack-setup-cmd_prefix|): +zpack provides a single user command, `:ZPack`, with subcommands. The +command name is configurable via |zpack-setup-cmd_name| (default: `ZPack`). - *:ZUpdate* -:ZUpdate [plugin] + *:ZPack* +:ZPack {subcommand} [args] + Dispatches to one of the subcommands listed below. Subcommand + names support tab completion, as do plugin-name arguments + where applicable. The bang form `:ZPack!` applies to the + subcommands that accept it (`build`, `load`, `delete`). + +:ZPack update [plugin] Update all plugins that are currently in your spec, or update a specific plugin if a name is provided. Supports tab completion of installed plugin names. See |vim.pack.update()|. - *:ZRestore* -:ZRestore [plugin] +:ZPack restore [plugin] Restore all plugins or a specific plugin to the state - recorded in the lockfile. This calls - |vim.pack.update()| with `target = 'lockfile'`. Useful - for synchronizing plugins across machines or reverting - after an update. Requires a lockfile to exist (created - automatically by |:ZUpdate|). Displays an error if no - lockfile exists or the restore otherwise fails. + recorded in the lockfile. This calls |vim.pack.update()| with + `target = 'lockfile'`. Useful for synchronizing plugins across + machines or reverting after an update. Requires a lockfile to + exist (created automatically by `:ZPack update`). Displays an + error if no lockfile exists or the restore otherwise fails. Supports tab completion of registered plugin names. - *:ZClean* -:ZClean Remove plugins that are no longer in your spec. This compares +:ZPack clean Remove plugins that are no longer in your spec. This compares installed plugins with your current spec and deletes any that are not referenced. - *:ZBuild* -:ZBuild[!] [plugin] +:ZPack[!] build [plugin] Run build hook for a specific plugin by name, or run build hooks for all plugins when called with ! and no plugin name. Without the !, attempting to build all will show a warning @@ -120,21 +122,19 @@ zpack provides the following commands (default prefix: `Z`, customizable via hooks. This is useful for manually rebuilding plugins without needing to update them. - *:ZLoad* -:ZLoad[!] [plugin] +:ZPack[!] load [plugin] Load a specific unloaded plugin by name, or load all unloaded plugins when called with ! and no plugin name. Without the !, attempting to load all will show a warning message. Supports tab completion of unloaded plugin names. - *:ZDelete* -:ZDelete[!] [plugin] +:ZPack[!] delete [plugin] Remove a specific plugin by name, or remove all installed plugins when called with ! and no plugin name. Without the !, - attempting to delete all will show a warning message. Supports - tab completion of installed plugin names. Deleting active - plugins in your spec can result in errors in your current - session. Restart Neovim to re-install them. + attempting to delete all will show a warning message. + Supports tab completion of installed plugin names. Deleting + active plugins in your spec can result in errors in your + current session. Restart Neovim to re-install them. ------------------------------------------------------------------------------ 4.3 CONFIGURATIONS *zpack-configurations* @@ -150,7 +150,7 @@ zpack provides the following commands (default prefix: `Z`, customizable via performance = { vim_loader = true, -- enables vim.loader for faster startup (default: true) }, - cmd_prefix = 'Z', -- :ZUpdate, :ZClean, etc. (default: 'Z') + cmd_name = 'ZPack', -- :ZPack update, :ZPack clean, etc. (default: 'ZPack') }) < Plugin-level settings always take precedence over `defaults`. @@ -209,16 +209,14 @@ spec (table, optional) }) < - *zpack-setup-cmd_prefix* -cmd_prefix (string, optional) - Prefix for user commands. Change this if the default - commands conflict with other plugins. Default: `'Z'` + *zpack-setup-cmd_name* +cmd_name (string, optional) + Name of the single user command. Change this if the default + conflicts with another plugin. Must start with an uppercase + letter and contain only letters and digits. Default: `'ZPack'` Examples: - - `'Z'` creates `:ZUpdate`, `:ZClean`, `:ZBuild`, `:ZLoad`, - `:ZDelete` - - `'Pack'` creates `:PackUpdate`, `:PackClean`, etc. - - `''` creates `:Update`, `:Clean`, `:Build`, `:Load`, - `:Delete` + - `'ZPack'` creates `:ZPack update`, `:ZPack clean`, etc. + - `'MyPack'` creates `:MyPack update`, `:MyPack clean`, etc. defaults (table, optional) Default values that apply to all plugins. Plugin-level diff --git a/lua/zpack/api/init.lua b/lua/zpack/api/init.lua index 0890542..c3be783 100644 --- a/lua/zpack/api/init.lua +++ b/lua/zpack/api/init.lua @@ -30,7 +30,7 @@ local function derive_status(entry) end -- load_status wins over cond_result: a cond=false plugin can still end up -- loaded if it is pulled in as a required dependency or force-loaded via - -- `:ZLoad!`. Reporting "disabled" for an actually-loaded plugin would + -- `:ZPack! load`. Reporting "disabled" for an actually-loaded plugin would -- break UIs that key off status. if entry.load_status == 'loaded' or entry.load_status == 'loading' then return entry.load_status diff --git a/lua/zpack/commands.lua b/lua/zpack/commands.lua index 6db569d..86cb335 100644 --- a/lua/zpack/commands.lua +++ b/lua/zpack/commands.lua @@ -4,20 +4,11 @@ local hooks = require('zpack.hooks') local M = {} -local validate_prefix = function(prefix) - if prefix == '' then - return true - end - if not prefix:match('^%u[%a%d]*$') then +local validate_name = function(name) + if not name or name == '' then return false end - return true -end - -local create_command = function(name, fn, opts) - local ok, err = pcall(vim.api.nvim_create_user_command, name, fn, opts) - if not ok then - util.schedule_notify(('Failed to create command %s: %s'):format(name, err), vim.log.levels.ERROR) + if not name:match('^%u[%a%d]*$') then return false end return true @@ -92,48 +83,42 @@ M.clean_unused = function() vim.pack.del(to_delete) end ----@param prefix string -M.setup = function(prefix) - if not validate_prefix(prefix) then - util.schedule_notify( - ('Invalid cmd_prefix "%s": must be empty or start with uppercase letter and contain only letters/digits'):format( - prefix), - vim.log.levels.ERROR - ) - return - end +--------------------------------------------------------------------- +-- Subcommand handlers +--------------------------------------------------------------------- - local complete_registered = function(arg_lead) - return filter_completions(state.registered_plugin_names, arg_lead) - end +local Sub = {} - create_command(prefix .. 'Update', function(opts) - run_pack_update(opts.args, nil, 'Update failed') - end, { - nargs = '?', - desc = 'Update all plugins or a specific plugin', - complete = complete_registered, - }) +Sub.update = { + run = function(ctx) + run_pack_update(ctx.arg, nil, 'Update failed') + end, + complete = function(arg_lead) + return filter_completions(state.registered_plugin_names, arg_lead) + end, +} - create_command(prefix .. 'Restore', function(opts) - run_pack_update(opts.args, { target = 'lockfile' }, 'Restore failed (have you run :' .. prefix .. 'Update?)') - end, { - nargs = '?', - desc = 'Restore all plugins or a specific plugin to lockfile state', - complete = complete_registered, - }) +Sub.restore = { + run = function(ctx) + run_pack_update(ctx.arg, { target = 'lockfile' }, ('Restore failed (have you run :%s update?)'):format(ctx.cmd_name)) + end, + complete = function(arg_lead) + return filter_completions(state.registered_plugin_names, arg_lead) + end, +} - create_command(prefix .. 'Clean', function() +Sub.clean = { + run = function() M.clean_unused() - end, { - desc = 'Remove unused plugins', - }) + end, +} - create_command(prefix .. 'Build', function(opts) - local plugin_name = opts.args +Sub.build = { + run = function(ctx) + local plugin_name = ctx.arg if plugin_name == '' then - if not opts.bang then - util.schedule_notify(('Use :%sBuild! to run build hooks for all plugins'):format(prefix), vim.log.levels.WARN) + if not ctx.bang then + util.schedule_notify(('Use :%s build! to run build hooks for all plugins'):format(ctx.cmd_name), vim.log.levels.WARN) return end hooks.run_all_builds() @@ -158,18 +143,18 @@ M.setup = function(prefix) end hooks.execute_build(spec.build, registry_entry.plugin) util.schedule_notify(('Running build hook for %s'):format(plugin_name), vim.log.levels.INFO) - end, { - nargs = '?', - bang = true, - desc = 'Run build hook for a specific plugin or all plugins', - complete = function(arg_lead) return filter_completions(state.plugin_names_with_build, arg_lead) end, - }) + end, + complete = function(arg_lead) + return filter_completions(state.plugin_names_with_build, arg_lead) + end, +} - create_command(prefix .. 'Load', function(opts) - local plugin_name = opts.args +Sub.load = { + run = function(ctx) + local plugin_name = ctx.arg if plugin_name == '' then - if not opts.bang then - util.schedule_notify(('Use :%sLoad! to load all unloaded plugins'):format(prefix), vim.log.levels.WARN) + if not ctx.bang then + util.schedule_notify(('Use :%s load! to load all unloaded plugins'):format(ctx.cmd_name), vim.log.levels.WARN) return end local count = vim.tbl_count(state.unloaded_plugin_names) @@ -207,26 +192,21 @@ M.setup = function(prefix) local loader = require('zpack.plugin_loader') loader.process_spec(pack.spec, {}) util.schedule_notify(('Loaded %s'):format(plugin_name), vim.log.levels.INFO) - end, { - nargs = '?', - bang = true, - desc = 'Load all unloaded plugins or a specific plugin', - complete = function(arg_lead) - local names = vim.tbl_keys(state.unloaded_plugin_names) - -- sorted on each invocation; negligible for typical plugin counts - table.sort(names, function(a, b) return a:lower() < b:lower() end) - return filter_completions(names, arg_lead) - end, - }) + end, + complete = function(arg_lead) + local names = vim.tbl_keys(state.unloaded_plugin_names) + -- sorted on each invocation; negligible for typical plugin counts + table.sort(names, function(a, b) return a:lower() < b:lower() end) + return filter_completions(names, arg_lead) + end, +} - create_command(prefix .. 'Delete', function(opts) - local plugin_name = opts.args +Sub.delete = { + run = function(ctx) + local plugin_name = ctx.arg if plugin_name == '' then - if not opts.bang then - util.schedule_notify( - ('Use :%sDelete! to confirm deletion of all installed plugin(s)'):format(prefix), - vim.log.levels.WARN - ) + if not ctx.bang then + util.schedule_notify(('Use :%s delete! to confirm deletion of all installed plugin(s)'):format(ctx.cmd_name), vim.log.levels.WARN) return end local names = {} @@ -256,12 +236,125 @@ M.setup = function(prefix) :format(plugin_name), vim.log.levels.WARN ) - end, { - nargs = '?', + end, + complete = function(arg_lead) + return filter_completions(state.registered_plugin_names, arg_lead) + end, +} + +-- Ordered list used for completion and usage messages. +local SUB_ORDER = { 'update', 'restore', 'clean', 'build', 'load', 'delete' } + +---@param cmd_name string +---@return function +local make_dispatcher = function(cmd_name) + return function(opts) + local fargs = opts.fargs or {} + local subname = fargs[1] + if not subname or subname == '' then + util.schedule_notify(('Usage: :%s {%s} [args]'):format(cmd_name, table.concat(SUB_ORDER, '|')), vim.log.levels.WARN) + return + end + + local sub = Sub[subname] + if not sub then + util.schedule_notify(('Unknown subcommand "%s". Available: %s'):format(subname, table.concat(SUB_ORDER, ', ')), vim.log.levels.ERROR) + return + end + + local arg = '' + if fargs[2] then + arg = table.concat(fargs, ' ', 2) + end + + sub.run({ + arg = arg, + bang = opts.bang, + cmd_name = cmd_name, + }) + end +end + +---@return string[] +local complete_command = function(arg_lead, cmd_line, _cursor_pos) + -- Strip leading " " (or "! ") from cmd_line for parsing. + local after_cmd = cmd_line:match('^%S+%s+(.*)$') or '' + local parts = vim.split(after_cmd, '%s+', { trimempty = false }) + + -- Completing the subcommand itself + if #parts <= 1 then + return filter_completions(SUB_ORDER, arg_lead) + end + + local subname = parts[1] + local sub = Sub[subname] + if not sub or not sub.complete then + return {} + end + return sub.complete(arg_lead) +end + +---@param cmd_name string +M.setup = function(cmd_name) + if not validate_name(cmd_name) then + util.schedule_notify(('Invalid cmd_name "%s": must start with uppercase letter and contain only letters/digits'):format(tostring(cmd_name)), vim.log.levels.ERROR) + return + end + + local ok, err = pcall(vim.api.nvim_create_user_command, cmd_name, make_dispatcher(cmd_name), { + nargs = '*', bang = true, - desc = 'Delete all plugins or a specific plugin', - complete = function(arg_lead) return filter_completions(state.registered_plugin_names, arg_lead) end, + desc = 'zpack: ' .. table.concat(SUB_ORDER, '|'), + complete = complete_command, }) + if not ok then + util.schedule_notify(('Failed to create command %s: %s'):format(cmd_name, err), vim.log.levels.ERROR) + end +end + +--------------------------------------------------------------------- +-- Legacy commands +--------------------------------------------------------------------- + +local LEGACY_COMMANDS = { + { suffix = 'Update', bang = false }, + { suffix = 'Restore', bang = false }, + { suffix = 'Clean', bang = false }, + { suffix = 'Build', bang = true }, + { suffix = 'Load', bang = true }, + { suffix = 'Delete', bang = true }, +} + +---@param prefix string Prefix to register legacy commands under (e.g. 'Z'). Empty string registers bare :Update/:Clean/etc. +M.setup_legacy = function(prefix) + -- Empty prefix is a valid back-compat case; otherwise enforce the same rules as cmd_name. + if prefix ~= '' and not validate_name(prefix) then return end + + local deprecation = require('zpack.deprecation') + + for _, entry in ipairs(LEGACY_COMMANDS) do + local cmd_name = prefix .. entry.suffix + local sub_name = entry.suffix:lower() + local sub = Sub[sub_name] + local cmd_opts = { + nargs = sub.complete and '?' or 0, + bang = entry.bang, + desc = ('[deprecated] use :ZPack %s instead'):format(sub_name), + } + + if sub.complete then + cmd_opts.complete = function(arg_lead) return sub.complete(arg_lead) end + end + + pcall(vim.api.nvim_create_user_command, cmd_name, function(opts) + deprecation.notify_deprecated_once('cmd_prefix', 'legacy_cmd:' .. cmd_name) + sub.run({ + arg = opts.args or '', + bang = opts.bang, + cmd_name = 'ZPack', + }) + end, cmd_opts) + end end return M diff --git a/lua/zpack/deprecation.lua b/lua/zpack/deprecation.lua index f77f403..9690ae0 100644 --- a/lua/zpack/deprecation.lua +++ b/lua/zpack/deprecation.lua @@ -32,6 +32,10 @@ M.deprecated = { message = "opts.plugins_dir is deprecated. Use { import = 'dir' } in spec instead:", replacement = "require('zpack').setup({ { import = 'plugins' } })", }, + cmd_prefix = { + message = "opts.cmd_prefix is deprecated. Use opts.cmd_name instead:", + replacement = "require('zpack').setup({ cmd_name = 'ZPack' })", + }, } M.notify_removed = function(key) @@ -52,4 +56,12 @@ M.notify_deprecated = function(key) ) end +local notified_once = {} +M.notify_deprecated_once = function(key, dedup_key) + dedup_key = dedup_key or key + if notified_once[dedup_key] then return end + notified_once[dedup_key] = true + M.notify_deprecated(key) +end + return M diff --git a/lua/zpack/init.lua b/lua/zpack/init.lua index 1adbf7e..c9ecc21 100644 --- a/lua/zpack/init.lua +++ b/lua/zpack/init.lua @@ -48,7 +48,7 @@ end ---@class zpack.Config ---@field spec? zpack.Spec[] ----@field cmd_prefix? string +---@field cmd_name? string Name of the single user command (default: 'ZPack') ---@field defaults? zpack.Config.Defaults ---@field performance? zpack.Config.Performance ---@field profiling? zpack.Config.Profiling @@ -57,7 +57,7 @@ end ---@field disable_vim_loader? boolean @deprecated Use performance.vim_loader instead local config = { - cmd_prefix = 'Z', + cmd_name = 'ZPack', defaults = { confirm = true }, performance = { vim_loader = true }, profiling = { loader = false, require = false }, @@ -101,8 +101,8 @@ M.setup = function(opts) opts = opts or {} local deprecation = require('zpack.deprecation') - if opts.cmd_prefix ~= nil then - config.cmd_prefix = opts.cmd_prefix + if opts.cmd_name ~= nil then + config.cmd_name = opts.cmd_name end if opts.defaults ~= nil then @@ -137,6 +137,9 @@ M.setup = function(opts) deprecation.notify_removed('auto_import') end + -- `cmd_prefix` is deprecated but will still work for a while + local legacy_prefix = opts.cmd_prefix ~= nil and opts.cmd_prefix or 'Z' + local ctx = create_context({ confirm = config.defaults.confirm, defaults = config.defaults }) local import = require('zpack.import') @@ -154,7 +157,8 @@ M.setup = function(opts) process_all(ctx) - require('zpack.commands').setup(config.cmd_prefix) + require('zpack.commands').setup(config.cmd_name) + require('zpack.commands').setup_legacy(legacy_prefix) end ---@deprecated Use setup({ spec = { ... } }) instead diff --git a/tests/cmd_name_test.lua b/tests/cmd_name_test.lua new file mode 100644 index 0000000..6efc4b1 --- /dev/null +++ b/tests/cmd_name_test.lua @@ -0,0 +1,128 @@ +local helpers = require('helpers') + +return function() + helpers.describe("Command Name Configuration", function() + helpers.test("default cmd_name creates :ZPack command", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false } }) + + local cmds = vim.api.nvim_get_commands({}) + helpers.assert_not_nil(cmds['ZPack'], "ZPack command should exist") + + helpers.cleanup_test_env() + end) + + helpers.test("custom cmd_name creates the configured command", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false }, cmd_name = 'Pack' }) + + local cmds = vim.api.nvim_get_commands({}) + helpers.assert_not_nil(cmds['Pack'], "Pack command should exist") + helpers.assert_nil(cmds['ZPack'], "Default ZPack command should not exist with custom name") + + helpers.cleanup_test_env() + helpers.delete_zpack_commands('Pack') + end) + + helpers.test("subcommand completion lists available subcommands", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false } }) + + local completions = vim.fn.getcompletion('ZPack ', 'cmdline') + helpers.assert_table_contains(completions, 'update', "update subcommand") + helpers.assert_table_contains(completions, 'restore', "restore subcommand") + helpers.assert_table_contains(completions, 'clean', "clean subcommand") + helpers.assert_table_contains(completions, 'build', "build subcommand") + helpers.assert_table_contains(completions, 'load', "load subcommand") + helpers.assert_table_contains(completions, 'delete', "delete subcommand") + + helpers.cleanup_test_env() + end) + + helpers.test("empty cmd_name is rejected", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false }, cmd_name = '' }) + + local cmds = vim.api.nvim_get_commands({}) + helpers.assert_nil(cmds['ZPack'], "ZPack command should not exist when cmd_name rejected") + + helpers.cleanup_test_env() + end) + + helpers.test("lowercase cmd_name is rejected", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false }, cmd_name = 'pack' }) + + local cmds = vim.api.nvim_get_commands({}) + helpers.assert_nil(cmds['pack'], "Commands with lowercase name should not be created") + helpers.assert_nil(cmds['ZPack'], "Default command should not be created") + + helpers.cleanup_test_env() + end) + + helpers.test("cmd_name with hyphen is rejected", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false }, cmd_name = 'My-Pack' }) + + local cmds = vim.api.nvim_get_commands({}) + helpers.assert_nil(cmds['My-Pack'], "Command with hyphen should not be created") + helpers.assert_nil(cmds['ZPack'], "Default command should not be created") + + helpers.cleanup_test_env() + end) + + helpers.test("cmd_name starting with digit is rejected", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false }, cmd_name = '123' }) + + local cmds = vim.api.nvim_get_commands({}) + helpers.assert_nil(cmds['123'], "Command starting with digit should not be created") + helpers.assert_nil(cmds['ZPack'], "Default command should not be created") + + helpers.cleanup_test_env() + end) + + helpers.test("cmd_name with special characters is rejected", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false }, cmd_name = 'Pack!' }) + + local cmds = vim.api.nvim_get_commands({}) + helpers.assert_nil(cmds['Pack!'], "Command with special char should not be created") + helpers.assert_nil(cmds['ZPack'], "Default command should not be created") + + helpers.cleanup_test_env() + end) + + helpers.test("cmd_name with whitespace is rejected", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false }, cmd_name = ' Z' }) + + local cmds = vim.api.nvim_get_commands({}) + helpers.assert_nil(cmds[' Z'], "Command with whitespace should not be created") + helpers.assert_nil(cmds['ZPack'], "Default command should not be created") + + helpers.cleanup_test_env() + end) + + helpers.test("cmd_name with digits after first letter is valid", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = {}, defaults = { confirm = false }, cmd_name = 'Z2' }) + + local cmds = vim.api.nvim_get_commands({}) + helpers.assert_not_nil(cmds['Z2'], "Z2 command should exist") + + helpers.cleanup_test_env() + helpers.delete_zpack_commands('Z2') + end) + end) +end diff --git a/tests/deprecation_test.lua b/tests/deprecation_test.lua index 308df32..00b52c0 100644 --- a/tests/deprecation_test.lua +++ b/tests/deprecation_test.lua @@ -62,6 +62,50 @@ return function() helpers.cleanup_test_env() end) + helpers.test("invoking a legacy :Z* command emits the cmd_prefix deprecation warning once", function() + helpers.setup_test_env() + + require('zpack').setup({ spec = { { 'test/plugin-a' } }, defaults = { confirm = false } }) + + helpers.flush_pending() + _G.test_state.notifications = {} + + vim.cmd('ZClean') + vim.cmd('ZClean') -- second invocation must NOT re-emit the warning + helpers.flush_pending() + + local count = 0 + for _, notif in ipairs(_G.test_state.notifications) do + if notif.msg:find("DEPRECATED") and notif.msg:find("cmd_prefix") then + count = count + 1 + end + end + helpers.assert_equal(count, 1, "deprecation warning must fire exactly once across repeated invocations") + + helpers.cleanup_test_env() + end) + + helpers.test("legacy :ZDelete delegates to the new delete subcommand with force=true", function() + helpers.setup_test_env() + + require('zpack').setup({ + spec = { { 'test/plugin-a' }, { 'test/plugin-b' } }, + defaults = { confirm = false }, + }) + + helpers.flush_pending() + + vim.cmd('ZDelete plugin-a') + helpers.flush_pending() + + helpers.assert_equal(#_G.test_state.vim_pack_del_calls, 1, "vim.pack.del must be called by legacy shim") + local call = _G.test_state.vim_pack_del_calls[1] + helpers.assert_true(call.opts.force, "legacy :ZDelete must propagate force=true to Sub.delete") + helpers.assert_table_contains(call.names, 'plugin-a', "legacy :ZDelete plugin-a must target plugin-a") + + helpers.cleanup_test_env() + end) + helpers.test("deprecated options still register plugins", function() helpers.setup_test_env() local state = require('zpack.state') diff --git a/tests/helpers.lua b/tests/helpers.lua index 9f6b434..c9f1cda 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -333,11 +333,14 @@ function M.cleanup_mock_plugin_dir(base_path) vim.fn.delete(base_path, 'rf') end -function M.delete_zpack_commands(prefix) - prefix = prefix or 'Z' - local commands = { 'Update', 'Clean', 'Build', 'Load', 'Restore', 'Delete' } - for _, cmd in ipairs(commands) do - pcall(vim.api.nvim_del_user_command, prefix .. cmd) +---@param cmd_name? string Primary command name registered by `setup` (default 'ZPack'). +---@param legacy_prefix? string Prefix for the deprecated : commands (default 'Z'). +function M.delete_zpack_commands(cmd_name, legacy_prefix) + cmd_name = cmd_name or 'ZPack' + legacy_prefix = legacy_prefix or 'Z' + pcall(vim.api.nvim_del_user_command, cmd_name) + for _, suffix in ipairs({ 'Update', 'Restore', 'Clean', 'Build', 'Load', 'Delete' }) do + pcall(vim.api.nvim_del_user_command, legacy_prefix .. suffix) end end diff --git a/tests/public_api_test.lua b/tests/public_api_test.lua index 015f22d..0c728a8 100644 --- a/tests/public_api_test.lua +++ b/tests/public_api_test.lua @@ -350,7 +350,7 @@ return function() -- Before lazy-load, alpha is pending. helpers.assert_equal(require('zpack').get_plugin('alpha').status, 'pending', "lazy plugin starts pending") - pcall(vim.cmd, 'ZLoad! alpha') + pcall(vim.cmd, 'ZPack! load alpha') helpers.assert_equal(observed_status, 'loading', "get_plugin inside config must see status=loading") helpers.assert_equal(require('zpack').get_plugin('alpha').status, 'loaded', "status should be loaded after load completes") @@ -564,9 +564,9 @@ return function() "cond=false plugin starts disabled" ) - -- `:ZLoad! alpha` force-loads past the cond gate (used e.g. when a + -- `:ZPack! load alpha` force-loads past the cond gate (used e.g. when a -- consumer deliberately wants the plugin loaded despite its cond). - pcall(vim.cmd, 'ZLoad! alpha') + pcall(vim.cmd, 'ZPack! load alpha') helpers.assert_equal( require('zpack').get_plugin('alpha').status, 'loaded', diff --git a/tests/run_all.lua b/tests/run_all.lua index 77c3bc1..2c46698 100644 --- a/tests/run_all.lua +++ b/tests/run_all.lua @@ -14,6 +14,7 @@ local test_modules = { 'priority_test', 'conditional_test', 'plugin_data_test', + 'cmd_name_test', 'cmd_prefix_test', 'version_test', 'deprecation_test', diff --git a/tests/zclean_test.lua b/tests/zclean_test.lua index 0d1588c..9d3e29d 100644 --- a/tests/zclean_test.lua +++ b/tests/zclean_test.lua @@ -1,8 +1,8 @@ local helpers = require('helpers') return function() - helpers.describe("ZClean Command", function() - helpers.test("ZClean detects orphan plugins not in spec", function() + helpers.describe("ZPack clean", function() + helpers.test("clean detects orphan plugins not in spec", function() helpers.setup_test_env() require('zpack').setup({ @@ -20,7 +20,7 @@ return function() name = 'orphan-plugin', } - vim.cmd('ZClean') + vim.cmd('ZPack clean') helpers.flush_pending() helpers.assert_equal(#_G.test_state.vim_pack_del_calls, 1, "vim.pack.del should be called") @@ -31,7 +31,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZClean does not delete plugins in spec", function() + helpers.test("clean does not delete plugins in spec", function() helpers.setup_test_env() require('zpack').setup({ @@ -44,7 +44,7 @@ return function() helpers.flush_pending() - vim.cmd('ZClean') + vim.cmd('ZPack clean') helpers.flush_pending() helpers.assert_equal(#_G.test_state.vim_pack_del_calls, 0, "vim.pack.del should not be called") @@ -61,7 +61,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZClean detects multiple orphan plugins", function() + helpers.test("clean detects multiple orphan plugins", function() helpers.setup_test_env() require('zpack').setup({ @@ -82,7 +82,7 @@ return function() name = 'orphan-2', } - vim.cmd('ZClean') + vim.cmd('ZPack clean') helpers.flush_pending() helpers.assert_equal(#_G.test_state.vim_pack_del_calls, 1, "vim.pack.del should be called once") diff --git a/tests/zdelete_test.lua b/tests/zdelete_test.lua index 13f2e3a..34cf48a 100644 --- a/tests/zdelete_test.lua +++ b/tests/zdelete_test.lua @@ -1,8 +1,8 @@ local helpers = require('helpers') return function() - helpers.describe("ZDelete Command", function() - helpers.test("ZDelete single plugin uses force=true", function() + helpers.describe("ZPack delete", function() + helpers.test("delete single plugin uses force=true", function() helpers.setup_test_env() require('zpack').setup({ @@ -15,7 +15,7 @@ return function() helpers.flush_pending() - vim.cmd('ZDelete plugin-a') + vim.cmd('ZPack delete plugin-a') helpers.flush_pending() helpers.assert_equal(#_G.test_state.vim_pack_del_calls, 1, "vim.pack.del should be called once") @@ -27,7 +27,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZDelete! all plugins uses force=true", function() + helpers.test("delete! all plugins uses force=true", function() helpers.setup_test_env() require('zpack').setup({ @@ -41,7 +41,7 @@ return function() helpers.flush_pending() - vim.cmd('ZDelete!') + vim.cmd('ZPack! delete') helpers.flush_pending() helpers.assert_equal(#_G.test_state.vim_pack_del_calls, 1, "vim.pack.del should be called once") @@ -54,7 +54,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZDelete without bang and no arg shows warning", function() + helpers.test("delete without bang and no arg shows warning", function() helpers.setup_test_env() require('zpack').setup({ @@ -66,7 +66,7 @@ return function() helpers.flush_pending() - vim.cmd('ZDelete') + vim.cmd('ZPack delete') helpers.flush_pending() helpers.assert_equal(#_G.test_state.vim_pack_del_calls, 0, "vim.pack.del should not be called") @@ -74,7 +74,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZDelete clears dependency graph entries for deleted plugin", function() + helpers.test("delete clears dependency graph entries for deleted plugin", function() helpers.setup_test_env() require('zpack').setup({ @@ -95,7 +95,7 @@ return function() helpers.assert_not_nil(state.dependency_graph[src_b], "plugin-b should have dependency graph entry") helpers.assert_not_nil(state.reverse_dependency_graph[src_b], "plugin-b should have reverse dependency graph entry") - vim.cmd('ZDelete plugin-b') + vim.cmd('ZPack delete plugin-b') helpers.flush_pending() helpers.assert_nil(state.dependency_graph[src_b], "plugin-b dependency graph entry should be cleared") @@ -114,7 +114,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZDelete clears src_to_pack_spec for deleted plugin", function() + helpers.test("delete clears src_to_pack_spec for deleted plugin", function() helpers.setup_test_env() require('zpack').setup({ @@ -132,7 +132,7 @@ return function() local src_b = 'https://github.com/test/plugin-b' helpers.assert_not_nil(state.src_to_pack_spec[src_a], "plugin-a should have src_to_pack_spec entry") - vim.cmd('ZDelete plugin-a') + vim.cmd('ZPack delete plugin-a') helpers.flush_pending() helpers.assert_nil(state.src_to_pack_spec[src_a], "plugin-a src_to_pack_spec entry should be cleared") @@ -141,7 +141,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZDelete non-existent plugin does not call vim.pack.del", function() + helpers.test("delete non-existent plugin does not call vim.pack.del", function() helpers.setup_test_env() require('zpack').setup({ @@ -153,7 +153,7 @@ return function() helpers.flush_pending() - vim.cmd('ZDelete non-existent-plugin') + vim.cmd('ZPack delete non-existent-plugin') helpers.flush_pending() helpers.assert_equal(#_G.test_state.vim_pack_del_calls, 0, "vim.pack.del should not be called for non-existent plugin") diff --git a/tests/zload_test.lua b/tests/zload_test.lua index 4c31240..c16d2f9 100644 --- a/tests/zload_test.lua +++ b/tests/zload_test.lua @@ -1,14 +1,14 @@ local helpers = require('helpers') return function() - helpers.describe("ZLoad Command", function() - helpers.test("ZLoad command is created with default prefix", function() + helpers.describe("ZPack load", function() + helpers.test("ZPack command is created with default name", function() helpers.setup_test_env() require('zpack').setup({ spec = {}, defaults = { confirm = false } }) local cmds = vim.api.nvim_get_commands({}) - helpers.assert_not_nil(cmds['ZLoad'], "ZLoad command should exist") + helpers.assert_not_nil(cmds['ZPack'], "ZPack command should exist") helpers.cleanup_test_env() end) @@ -90,7 +90,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZLoad without bang shows warning", function() + helpers.test("load without bang shows warning", function() helpers.setup_test_env() require('zpack').setup({ @@ -106,22 +106,22 @@ return function() helpers.flush_pending() _G.test_state.notifications = {} - pcall(vim.cmd, 'ZLoad') + pcall(vim.cmd, 'ZPack load') helpers.flush_pending() local found_warning = false for _, notif in ipairs(_G.test_state.notifications) do - if notif.msg:find('ZLoad!') and notif.level == vim.log.levels.WARN then + if notif.msg:find('load!') and notif.level == vim.log.levels.WARN then found_warning = true break end end - helpers.assert_true(found_warning, "Should show warning about using ZLoad!") + helpers.assert_true(found_warning, "Should show warning about using load!") helpers.cleanup_test_env() end) - helpers.test("ZLoad! loads all unloaded plugins", function() + helpers.test("load! loads all unloaded plugins", function() helpers.setup_test_env() local state = require('zpack.state') local config_called = {} @@ -145,7 +145,7 @@ return function() helpers.flush_pending() helpers.assert_equal(vim.tbl_count(state.unloaded_plugin_names), 2, "Should have 2 unloaded plugins") - vim.cmd('ZLoad!') + vim.cmd('ZPack! load') helpers.flush_pending() helpers.assert_equal(vim.tbl_count(state.unloaded_plugin_names), 0, "Should have 0 unloaded plugins") @@ -155,7 +155,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZLoad! with no unloaded plugins shows info message", function() + helpers.test("load! with no unloaded plugins shows info message", function() helpers.setup_test_env() require('zpack').setup({ @@ -171,7 +171,7 @@ return function() helpers.flush_pending() _G.test_state.notifications = {} - vim.cmd('ZLoad!') + vim.cmd('ZPack! load') helpers.flush_pending() local found_info = false @@ -186,7 +186,7 @@ return function() helpers.cleanup_test_env() end) - helpers.test("ZLoad already loaded plugin shows info message", function() + helpers.test("load already-loaded plugin shows info message", function() helpers.setup_test_env() require('zpack').setup({ @@ -202,7 +202,7 @@ return function() helpers.flush_pending() _G.test_state.notifications = {} - pcall(vim.cmd, 'ZLoad startup-plugin') + pcall(vim.cmd, 'ZPack load startup-plugin') helpers.flush_pending() local found_info = false diff --git a/tests/zrestore_test.lua b/tests/zrestore_test.lua index accac8b..1c51145 100644 --- a/tests/zrestore_test.lua +++ b/tests/zrestore_test.lua @@ -1,7 +1,7 @@ local pack_update_tests = require('pack_update_test_helpers') return pack_update_tests.create_tests({ - command = 'ZRestore', + command = 'ZPack restore', expected_opts = { target = 'lockfile' }, error_prefix = 'Restore failed', }) diff --git a/tests/zupdate_test.lua b/tests/zupdate_test.lua index 9d7c32a..0415be0 100644 --- a/tests/zupdate_test.lua +++ b/tests/zupdate_test.lua @@ -1,7 +1,7 @@ local pack_update_tests = require('pack_update_test_helpers') return pack_update_tests.create_tests({ - command = 'ZUpdate', + command = 'ZPack update', expected_opts = nil, error_prefix = 'Update failed', })