Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ Code yanks are syntax-highlighted when ClipRing can detect a language (markdown
| `<Enter>` | 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 `<Esc>` | Close without pasting |
| `C` | Clear all entries (asks for confirmation: `y` yes, `n` cancel) |
| `q` or `<Esc>` | Close without pasting (`Esc` cancels a pending clear-all first) |

While the picker is focused, `<C-w>` 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).

Expand Down Expand Up @@ -114,6 +115,7 @@ require("clipring").setup({
reorder_down_mapping = "<C-j>",
reorder_up_mapping = "<C-k>",
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
Expand All @@ -127,7 +129,9 @@ require("clipring").setup({

**`open_mapping`** — set a string (e.g. `"<leader>y"`) or multiple (`{ "<leader>y", "<M-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.

Expand Down Expand Up @@ -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 / `<C-w>` 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 / `<C-w>` 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

Expand Down
2 changes: 2 additions & 0 deletions lua/clipring/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ local M = {}
---@field reorder_down_mapping string|false|nil move selected entry down in picker (default `<C-j>`)
---@field reorder_up_mapping string|false|nil move selected entry up in picker (default `<C-k>`)
---@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)
Expand All @@ -29,6 +30,7 @@ M.defaults = {
reorder_down_mapping = "<C-j>",
reorder_up_mapping = "<C-k>",
copy_mapping = "y",
clear_all_mapping = "C",
picker_width = 80,
list_width = 0,
preview_max_width = 0,
Expand Down
90 changes: 88 additions & 2 deletions lua/clipring/ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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("<Esc>", function()
if state.clear_all_confirm then
cancel_clear_all_confirm()
return
end
close()
end, "ClipRing: close")

Expand Down
37 changes: 37 additions & 0 deletions tests/ui_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down