diff --git a/README.md b/README.md index 68e5367..5ff8ab4 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ The picker opens as a centered floating window listing recent yanks (newest firs | `` / `` | Same as `k` / `j` | | `` / `` | Move the **selected entry** down / up in history order (reorder) | | `` | 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 | @@ -84,7 +85,7 @@ While the picker is focused, `` does not switch windows or open which-key ( 1. Yank text as usual (`y`, `yy`, visual yank, etc.). 2. Open ClipRing (`:ClipRing` or your mapping). 3. Use `j` / `k` to highlight an entry, optionally `` / `` to reorder favorites. -4. Press `` to paste, or `q` to cancel. +4. Press `` to paste, `y` to copy to the system clipboard without pasting, or `q` to cancel. With `persist = true`, history is restored after you restart Neovim (stored under `persist_path`). @@ -101,12 +102,15 @@ require("clipring").setup({ open_mapping = "y", -- string, list of strings, or false (nil = no keymap) reorder_down_mapping = "", -- picker: move entry down in history (false to disable) reorder_up_mapping = "", -- picker: move entry up in history (false to disable) + copy_mapping = "y", -- picker: copy to system clipboard (false to disable) }) ``` **`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` to keep the defaults above. Set either to `false` to turn off that binding. +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. + +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. If `` / `` conflict with global maps (e.g. `:move`), use different keys: `reorder_down_mapping = ""`. diff --git a/lua/clipring/config.lua b/lua/clipring/config.lua index 1004e61..71d5687 100644 --- a/lua/clipring/config.lua +++ b/lua/clipring/config.lua @@ -10,6 +10,7 @@ local M = {} ---@field open_mapping string|string[]|false|nil keymap(s) to open picker (`nil` = `:ClipRing` only) ---@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`) M.defaults = { max_entries = 100, @@ -21,6 +22,7 @@ M.defaults = { open_mapping = nil, reorder_down_mapping = "", reorder_up_mapping = "", + copy_mapping = "y", } ---@type ClipRingConfig diff --git a/lua/clipring/paste.lua b/lua/clipring/paste.lua index 14321b0..ca6af7f 100644 --- a/lua/clipring/paste.lua +++ b/lua/clipring/paste.lua @@ -307,4 +307,19 @@ function M.apply(entry, opener_mode, visual_marks, opener_win, opener_cursor) end end +--- Copy ring entry to unnamed and system clipboard registers without pasting. +---@param entry ClipRingEntry|nil +---@return boolean +function M.copy_to_clipboard(entry) + if not entry or not entry.lines or #entry.lines == 0 then + return false + end + local lines = entry.lines + local regtype = entry.regtype or "v" + vim.fn.setreg('"', lines, regtype) + vim.fn.setreg("+", lines, regtype) + vim.fn.setreg("*", lines, regtype) + return true +end + return M diff --git a/lua/clipring/ui.lua b/lua/clipring/ui.lua index 0a99533..9c769fb 100644 --- a/lua/clipring/ui.lua +++ b/lua/clipring/ui.lua @@ -139,6 +139,17 @@ local function select_current() paste.apply(entry, mode, marks, opener_win, opener_cursor) end +local function copy_current() + local all = ring.get_all() + if #all == 0 then + return + end + local entry = all[state.index] + if entry then + paste.copy_to_clipboard(entry) + end +end + local function delete_current() local all = ring.get_all() if #all == 0 then @@ -236,6 +247,12 @@ local function attach_keymaps() map("", function() select_current() end, "ClipRing: paste entry") + 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") + end map("dd", function() delete_current() end, "ClipRing: delete entry") diff --git a/tests/paste_spec.lua b/tests/paste_spec.lua index ac7f2e6..6ff88ac 100644 --- a/tests/paste_spec.lua +++ b/tests/paste_spec.lua @@ -150,4 +150,12 @@ describe("clipring.paste", function() assert.are_not.equal("hola mundofoo", h.buf_text(buf)) end) + it("copy_to_clipboard sets registers with regtype", function() + local entry = h.entry({ "one", "two" }, "V") + assert.is_true(paste.copy_to_clipboard(entry)) + -- Unnamed register is always available (headless has no OS clipboard for +). + assert.same({ "one", "two" }, vim.fn.getreg('"', 1, 1)) + assert.are.equal("V", vim.fn.getregtype('"')) + end) + end) diff --git a/tests/ui_spec.lua b/tests/ui_spec.lua index 7b4f758..7fabed9 100644 --- a/tests/ui_spec.lua +++ b/tests/ui_spec.lua @@ -107,6 +107,30 @@ describe("clipring.ui", function() ui.close() end) + it("maps y to copy the selected entry to clipboard registers without closing", function() + ui.open() + local clip_buf = feed_clipring("j") + assert.are.equal(2, h.clipring_selected_line(clip_buf)) + feed_clipring("y") + assert.are.equal("older", vim.fn.getreg('"')) + assert.is_true(vim.fn.bufwinid(clip_buf) > 0) + ui.close() + end) + + it("maps custom copy key from config", function() + require("clipring.config").setup({ + max_entries = 20, + deduplicate = true, + min_length = 1, + persist = false, + copy_mapping = "c", + }) + ui.open() + feed_clipring("c") + assert.are.equal("newer", vim.fn.getreg('"')) + ui.close() + end) + it("maps Ctrl-j and Ctrl-k to reorder yanks in the ring", function() ui.open() assert.are.equal("newer", ring.get(1).lines[1])