From 859fc5c777172a7083805847bb7a78b2f1577182 Mon Sep 17 00:00:00 2001 From: Masaaki Hirotsu Date: Wed, 3 Jun 2026 11:34:16 +0900 Subject: [PATCH] feat(commands): filter command completion by ArgLead and add delete picker User command completion implemented as a Lua function behaves like customlist, so Neovim never narrows the returned candidates against the typed prefix. Add filter_by_prefix and apply it to the PeekstackRestoreSession, PeekstackDeleteSession, and PeekstackQuickPeek completions so only prefix-matching names are offered; the QuickPeek provider list already arrives sorted from the registry, so filtering preserves that order. PeekstackDeleteSession now takes an optional name and, when omitted, lists saved sessions via vim.ui.select before confirming, matching PeekstackListSessions. --- README.md | 2 +- doc/peekstack.txt | 5 +- lua/peekstack/commands.lua | 69 +++++++++++++++++---- tests/commands_spec.lua | 120 +++++++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 16 deletions(-) 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)