diff --git a/README.md b/README.md index d4068e1..63d02ba 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Built-in provider names: - `:PeekstackSaveSession` — save current stack (persist enabled) - `:PeekstackRestoreSession` — restore a saved session - `:PeekstackListSessions` — list all saved sessions -- `:PeekstackDeleteSession {name}` — delete a saved session by name +- `:PeekstackDeleteSession [name]` — delete a saved session (prompts to select when no name is given) - `:PeekstackRestorePopup` — restore the last closed popup (undo close) - `:PeekstackRestoreAllPopups` — restore all closed popups - `:PeekstackCloseAll` — close all popups in the current stack diff --git a/doc/peekstack.txt b/doc/peekstack.txt index d8e2e94..7aaec7e 100644 --- a/doc/peekstack.txt +++ b/doc/peekstack.txt @@ -279,8 +279,9 @@ Commands are registered after `setup()` is called. :PeekstackListSessions *:PeekstackListSessions* List all saved sessions. Select one to view its summary. -:PeekstackDeleteSession {name} *:PeekstackDeleteSession* - Delete a saved session by name. Prompts for confirmation. +:PeekstackDeleteSession [name] *:PeekstackDeleteSession* + Delete a saved session. Prompts for confirmation before deleting. When + no name is given, prompts to select one from the saved sessions. :PeekstackRestorePopup *:PeekstackRestorePopup* Restore the last closed popup from history (undo close). diff --git a/lua/peekstack/commands.lua b/lua/peekstack/commands.lua index 9b3a495..59c5ffd 100644 --- a/lua/peekstack/commands.lua +++ b/lua/peekstack/commands.lua @@ -46,6 +46,37 @@ local function list_session_names() return names end +---Keep only candidates whose prefix matches the current argument. +---Lua command completion behaves like `customlist`, so Neovim does not filter +---the returned list against `ArgLead` for us. +---@param candidates string[] +---@param arg_lead string +---@return string[] +local function filter_by_prefix(candidates, arg_lead) + if not arg_lead or arg_lead == "" then + return candidates + end + return vim.tbl_filter(function(candidate) + return vim.startswith(candidate, arg_lead) + end, candidates) +end + +---@param arg_lead string +---@return string[] +local function complete_session_names(arg_lead) + return filter_by_prefix(list_session_names(), arg_lead) +end + +---Confirm and delete a named session. +---@param name string +local function confirm_delete_session(name) + vim.ui.select({ "Yes", "No" }, { prompt = "Delete session '" .. name .. "'?" }, function(choice) + if choice == "Yes" then + require("peekstack.persist").delete_session(name) + end + end) +end + function M.setup() if loaded then return @@ -80,7 +111,7 @@ function M.setup() require("peekstack.persist").restore(name) end, { nargs = "?", - complete = list_session_names, + complete = complete_session_names, }) vim.api.nvim_create_user_command("PeekstackListSessions", function() @@ -115,19 +146,31 @@ function M.setup() end, {}) vim.api.nvim_create_user_command("PeekstackDeleteSession", function(opts) - local name = opts.args - if not name or name == "" then - notify.warn("Usage: PeekstackDeleteSession ") + local name = opts.args and opts.args ~= "" and opts.args or nil + if name then + confirm_delete_session(name) return end - vim.ui.select({ "Yes", "No" }, { prompt = "Delete session '" .. name .. "'?" }, function(choice) - if choice == "Yes" then - require("peekstack.persist").delete_session(name) - end - end) + + require("peekstack.persist").list_sessions({ + on_done = function(sessions) + local names = vim.tbl_keys(sessions) + table.sort(names) + if #names == 0 then + notify.info("No saved sessions") + return + end + vim.ui.select(names, { prompt = "Delete session" }, function(selected) + if not selected then + return + end + confirm_delete_session(selected) + end) + end, + }) end, { - nargs = 1, - complete = list_session_names, + nargs = "?", + complete = complete_session_names, }) vim.api.nvim_create_user_command("PeekstackRestorePopup", function() @@ -201,8 +244,8 @@ function M.setup() require("peekstack").peek(provider, { mode = "quick" }) end, { nargs = "?", - complete = function() - return require("peekstack").list_providers() + complete = function(arg_lead) + return filter_by_prefix(require("peekstack").list_providers(), arg_lead) end, }) end diff --git a/tests/commands_spec.lua b/tests/commands_spec.lua index befc4d2..3902875 100644 --- a/tests/commands_spec.lua +++ b/tests/commands_spec.lua @@ -4,6 +4,7 @@ describe("peekstack.commands", function() local peekstack = require("peekstack") local persist = require("peekstack.persist") local original_list_sessions = nil + local original_delete_session = nil local original_select = nil local original_notify = nil local original_strftime = nil @@ -12,6 +13,7 @@ describe("peekstack.commands", function() config.setup({}) commands._reset() original_list_sessions = persist.list_sessions + original_delete_session = persist.delete_session original_select = vim.ui.select original_notify = vim.notify original_strftime = vim.fn.strftime @@ -22,6 +24,9 @@ describe("peekstack.commands", function() if original_list_sessions then persist.list_sessions = original_list_sessions end + if original_delete_session then + persist.delete_session = original_delete_session + end if original_select then vim.ui.select = original_select end @@ -54,6 +59,121 @@ describe("peekstack.commands", function() assert.is_false(called_with_opts) end) + it("filters session completion by ArgLead prefix", function() + persist.list_sessions = function() + return { + alpha = { items = {}, meta = { created_at = 1, updated_at = 1 } }, + alpine = { items = {}, meta = { created_at = 1, updated_at = 1 } }, + beta = { items = {}, meta = { created_at = 1, updated_at = 1 } }, + } + end + + commands.setup() + local names = vim.fn.getcompletion("PeekstackRestoreSession al", "cmdline") + table.sort(names) + + assert.same({ "alpha", "alpine" }, names) + end) + + it("filters quick peek completion by ArgLead prefix", function() + peekstack.setup({}) + local names = vim.fn.getcompletion("PeekstackQuickPeek lsp.d", "cmdline") + + assert.is_true(#names > 0) + for _, name in ipairs(names) do + assert.is_true(vim.startswith(name, "lsp.d")) + end + assert.is_true(vim.list_contains(names, "lsp.definition")) + assert.is_true(vim.list_contains(names, "lsp.declaration")) + assert.is_false(vim.list_contains(names, "lsp.references")) + end) + + it("prompts to select a session when delete is invoked without a name", function() + local deleted = nil + local select_items = nil + persist.list_sessions = function(opts) + assert.is_truthy(opts) + assert.is_truthy(opts.on_done) + opts.on_done({ + beta = { items = {}, meta = { created_at = 1, updated_at = 1 } }, + alpha = { items = {}, meta = { created_at = 1, updated_at = 1 } }, + }) + return {} + end + persist.delete_session = function(name) + deleted = name + end + + vim.ui.select = function(items, opts, on_choice) + if opts.prompt == "Delete session" then + select_items = vim.deepcopy(items) + on_choice("beta") + return + end + on_choice("Yes") + end + + commands.setup() + vim.api.nvim_cmd({ cmd = "PeekstackDeleteSession" }, {}) + + assert.same({ "alpha", "beta" }, select_items) + assert.equals("beta", deleted) + end) + + it("notifies when no sessions exist on nameless delete", function() + local deleted = false + local messages = {} + persist.list_sessions = function(opts) + opts.on_done({}) + return {} + end + persist.delete_session = function() + deleted = true + end + vim.notify = function(msg) + table.insert(messages, msg) + end + + commands.setup() + vim.api.nvim_cmd({ cmd = "PeekstackDeleteSession" }, {}) + + assert.is_true(vim.list_contains(messages, "[peekstack] No saved sessions")) + assert.is_false(deleted) + end) + + it("confirms before deleting a named session", function() + local deleted = nil + local prompt = nil + persist.delete_session = function(name) + deleted = name + end + vim.ui.select = function(_items, opts, on_choice) + prompt = opts.prompt + on_choice("Yes") + end + + commands.setup() + vim.api.nvim_cmd({ cmd = "PeekstackDeleteSession", args = { "alpha" } }, {}) + + assert.equals("Delete session 'alpha'?", prompt) + assert.equals("alpha", deleted) + end) + + it("does not delete a named session when confirmation is declined", function() + local deleted = false + persist.delete_session = function() + deleted = true + end + vim.ui.select = function(_items, _opts, on_choice) + on_choice("No") + end + + commands.setup() + vim.api.nvim_cmd({ cmd = "PeekstackDeleteSession", args = { "alpha" } }, {}) + + assert.is_false(deleted) + end) + it("handles missing session meta in list command", function() local prompts = {} persist.list_sessions = function(opts)