From 59ecb75f60a287ae5ef3c4e6cfd8a06edbcb0d0b Mon Sep 17 00:00:00 2001 From: Alejandro Espinoza Date: Mon, 15 Jun 2026 11:32:52 -0600 Subject: [PATCH] feat(ui): clear all yank history from the picker with confirmation. Add clear_all_mapping (default C); confirm with y, cancel with n or Esc. Co-authored-by: Cursor --- README.md | 12 ++++-- lua/clipring/config.lua | 2 + lua/clipring/ui.lua | 90 ++++++++++++++++++++++++++++++++++++++++- tests/ui_spec.lua | 37 +++++++++++++++++ 4 files changed, 135 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index eeae2a0..c69007c 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,8 @@ Code yanks are syntax-highlighted when ClipRing can detect a language (markdown | `` | Paste the selected entry and close | | `y` | Copy the selected entry to the system clipboard (`+` / `*`) and keep the picker open | | `dd` | Delete the selected entry from history | -| `q` or `` | Close without pasting | +| `C` | Clear all entries (asks for confirmation: `y` yes, `n` cancel) | +| `q` or `` | Close without pasting (`Esc` cancels a pending clear-all first) | While the picker is focused, `` does not switch windows or open which-key (close the picker first, like Telescope). Keys apply to the history list; the preview pane is read-only. If you use [which-key.nvim](https://github.com/folke/which-key.nvim), `setup()` disables which-key on the history list buffer (`clipring` filetype). @@ -114,6 +115,7 @@ require("clipring").setup({ reorder_down_mapping = "", reorder_up_mapping = "", copy_mapping = "y", + clear_all_mapping = "C", -- Layout (list position is fixed; preview auto-sizes within these limits) picker_width = 80, -- total inner width; 0 = nearly full editor width @@ -127,7 +129,9 @@ require("clipring").setup({ **`open_mapping`** — set a string (e.g. `"y"`) or multiple (`{ "y", "" }`) to open ClipRing from Normal, Visual, and Insert. Leave unset or `nil` to use only `:ClipRing`. Use `false` to clear a keymap after a previous `setup()`. -Omit `reorder_down_mapping` / `reorder_up_mapping` / `copy_mapping` to keep the defaults above. Set any of them to `false` to turn off that binding. +Omit `reorder_down_mapping` / `reorder_up_mapping` / `copy_mapping` / `clear_all_mapping` to keep the defaults above. Set any of them to `false` to turn off that binding. + +During clear-all confirmation, `y` confirms (same key as `copy_mapping` when not confirming) and `n` cancels. Copy uses Neovim’s `+` and `*` registers (and the unnamed `"` register). You need clipboard support in Neovim (`:checkhealth clipboard`); on remote SSH, OSC52 or a clipboard provider may be required. @@ -171,13 +175,13 @@ Coverage today: - **ring** — add, dedupe, max size, remove, reorder - **preview_syntax** — fence stripping, language detection, heuristics - **paste** — visual capture (`v` / `'<`), charwise replace vs append, insert-mode paste at saved cursor -- **ui** — picker from insert, navigation, wrap-around selection, fixed list layout, preview resize/restore, conditional preview, syntax highlighting, clipboard copy, which-key / `` behavior +- **ui** — picker from insert, navigation, wrap-around selection, fixed list layout, preview resize/restore, conditional preview, clear all with confirmation, syntax highlighting, clipboard copy, which-key / `` behavior - **yank** — `TextYankPost` capture - **setup** — `open_mapping` registration ## Roadmap -Possible future work: Telescope picker, bulk delete. +Possible future work: Telescope picker, selective bulk delete. ## License diff --git a/lua/clipring/config.lua b/lua/clipring/config.lua index a746fe0..4b75d50 100644 --- a/lua/clipring/config.lua +++ b/lua/clipring/config.lua @@ -11,6 +11,7 @@ local M = {} ---@field reorder_down_mapping string|false|nil move selected entry down in picker (default ``) ---@field reorder_up_mapping string|false|nil move selected entry up in picker (default ``) ---@field copy_mapping string|false|nil copy selected entry to system clipboard in picker (default `y`) +---@field clear_all_mapping string|false|nil clear entire history in picker with confirmation (default `C`) ---@field picker_width number total inner width of list + preview (`0` = nearly full editor width) ---@field list_width number width of the history list (`0` = auto; fixed width is advanced) ---@field preview_max_width number max preview width (`0` = content width up to screen edge) @@ -29,6 +30,7 @@ M.defaults = { reorder_down_mapping = "", reorder_up_mapping = "", copy_mapping = "y", + clear_all_mapping = "C", picker_width = 80, list_width = 0, preview_max_width = 0, diff --git a/lua/clipring/ui.lua b/lua/clipring/ui.lua index 3be36d9..ab9978a 100644 --- a/lua/clipring/ui.lua +++ b/lua/clipring/ui.lua @@ -21,6 +21,7 @@ local state = { list_row = 0, list_height = 3, float_gap = 1, + clear_all_confirm = false, } local ns = vim.api.nvim_create_namespace("ClipRing") @@ -106,6 +107,18 @@ local function refresh_list_buffer() local lines = {} local all = ring.get_all() + + if state.clear_all_confirm and #all > 0 then + local n = #all + local word = n == 1 and "entry" or "entries" + lines = { + string.format("Clear all %d %s?", n, word), + " y = yes n = cancel", + } + set_buf_lines(state.list_buf, lines) + return + end + if #all == 0 then lines = { "No yanks yet. Copy something with y or Y." } state.index = 1 @@ -198,6 +211,9 @@ local function entry_has_preview_content(entry) end local function preview_should_show() + if state.clear_all_confirm then + return false + end return ring.count() > 0 and entry_has_preview_content(ring.get(state.index)) end @@ -462,6 +478,7 @@ local function close_windows_and_bufs() state.list_win = nil state.preview_buf = nil state.preview_win = nil + state.clear_all_confirm = false end local function close(restore_insert) @@ -491,6 +508,10 @@ local function focus_list_normal() end local function select_current() + if state.clear_all_confirm then + return + end + local all = ring.get_all() if #all == 0 then close() @@ -528,7 +549,39 @@ local function copy_current() end end +local function cancel_clear_all_confirm() + if not state.clear_all_confirm then + return + end + state.clear_all_confirm = false + refresh_buffers() +end + +local function request_clear_all_confirm() + if ring.count() == 0 or state.clear_all_confirm then + return + end + state.clear_all_confirm = true + refresh_buffers() +end + +local function confirm_clear_all() + if not state.clear_all_confirm then + return + end + ring.clear() + persist.save() + state.clear_all_confirm = false + state.index = 1 + refresh_buffers() +end + local function delete_current() + if state.clear_all_confirm then + cancel_clear_all_confirm() + return + end + local all = ring.get_all() if #all == 0 then return @@ -547,6 +600,11 @@ local function delete_current() end local function move_selection(delta) + if state.clear_all_confirm then + cancel_clear_all_confirm() + return + end + local count = ring.count() if count == 0 then return @@ -556,6 +614,11 @@ local function move_selection(delta) end local function reorder_current(delta) + if state.clear_all_confirm then + cancel_clear_all_confirm() + return + end + if ring.count() == 0 then return end @@ -627,16 +690,39 @@ local function attach_keymaps() local copy_key = picker_mapping("copy_mapping", "y") if copy_key then map(copy_key, function() - copy_current() - end, "ClipRing: copy entry to system clipboard") + if state.clear_all_confirm then + confirm_clear_all() + else + copy_current() + end + end, "ClipRing: copy entry or confirm clear all") end map("dd", function() delete_current() end, "ClipRing: delete entry") + local clear_all = picker_mapping("clear_all_mapping", "C") + if clear_all then + map(clear_all, function() + request_clear_all_confirm() + end, "ClipRing: clear all entries") + end + map("n", function() + if state.clear_all_confirm then + cancel_clear_all_confirm() + end + end, "ClipRing: cancel clear all") map("q", function() + if state.clear_all_confirm then + cancel_clear_all_confirm() + return + end close() end, "ClipRing: close") map("", function() + if state.clear_all_confirm then + cancel_clear_all_confirm() + return + end close() end, "ClipRing: close") diff --git a/tests/ui_spec.lua b/tests/ui_spec.lua index b6e0bb7..2c8e11f 100644 --- a/tests/ui_spec.lua +++ b/tests/ui_spec.lua @@ -285,6 +285,43 @@ describe("clipring.ui", function() ui.close() end) + it("prompts to clear all entries and clears on confirm", function() + ui.open() + feed_clipring("C") + local clip_buf = h.find_clipring_buf() + local lines = vim.api.nvim_buf_get_lines(clip_buf, 0, -1, false) + assert.matches("Clear all 2 entries", lines[1]) + feed_clipring("y") + assert.are.equal(0, ring.count()) + assert.matches("No yanks yet", vim.api.nvim_buf_get_lines(clip_buf, 0, -1, false)[1]) + ui.close() + end) + + it("cancels clear all on n or Esc", function() + ui.open() + feed_clipring("C") + feed_clipring("n") + assert.are.equal(2, ring.count()) + feed_clipring("C") + feed_clipring("\27") + assert.are.equal(2, ring.count()) + ui.close() + end) + + it("can disable clear all mapping from config", function() + require("clipring.config").setup({ + max_entries = 20, + deduplicate = true, + min_length = 1, + persist = false, + clear_all_mapping = false, + }) + ui.open() + feed_clipring("C") + assert.are.equal(2, ring.count()) + ui.close() + end) + it("maps custom copy key from config", function() require("clipring.config").setup({ max_entries = 20,