diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt index 227a9bb6b0..a246779731 100644 --- a/runtime/doc/repeat.txt +++ b/runtime/doc/repeat.txt @@ -447,8 +447,13 @@ LIMITATIONS *mcursor-limitations* - During Insert-mode completion, live mirroring pauses if a preview could shift or overlap the primary's completion (same-line edits before the primary, or line insertions/deletions before it). -- Cursors on the same line can be lost or misplaced by an edit that shifts - columns (e.g. inserting text). +- Explicit clipboard registers (`"*yy`, `"+yy`) are global, not per-cursor. On + multicursor exit, you can set the clipboard to the joined yank: > + :let @+=@" +- Implicit 'clipboard' ("unnamed"/"unnamedplus") updates the clipboard on + multicursor exit. +- Same-line cursors may be lost/misplaced by an edit that shifts columns (e.g. + inserting text). - Time-travel undo (|g-|, |g+|, |:earlier|) clears all cursors. - Reloading/unloading a buffer (|:edit!|, 'autoread') clears its cursors (extmarks limitation). diff --git a/src/nvim/clipboard.c b/src/nvim/clipboard.c index f58ed7d367..6ac08b0836 100644 --- a/src/nvim/clipboard.c +++ b/src/nvim/clipboard.c @@ -7,6 +7,7 @@ #include "nvim/clipboard.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" +#include "nvim/mcursor.h" #include "nvim/option_vars.h" #include "nvim/register.h" @@ -26,8 +27,8 @@ static bool clipboard_didwarn = false; /// @param quiet Suppress error messages /// @param writing if we're setting the contents of the clipboard /// -/// @returns the yankreg that should be written into, or `NULL` -/// if the register isn't a clipboard or provider isn't available. +/// @return The yankreg that should be written-to/read-from, or `NULL` if the register isn't +/// a clipboard or provider isn't available. yankreg_T *adjust_clipboard_name(int *name, bool quiet, bool writing) { #define MSG_NO_CLIP "clipboard: No provider. " \ @@ -84,6 +85,7 @@ end: bool get_clipboard(int name, yankreg_T **target, bool quiet) { + const bool implicit = name == NUL; // clipboard=unnamed[plus] // show message on error bool errmsg = true; @@ -91,6 +93,13 @@ bool get_clipboard(int name, yankreg_T **target, bool quiet) if (reg == NULL) { return false; } + if (mc_replaying()) { // Multicursor cascade: don't read the provider. + if (implicit) { + return false; // clipboard=unnamed[plus] behaves as (cursor-local) unnamed register. + } + *target = reg; // Explicit "+/"* gets the primary cursor's (cached) clipboard value. + return true; + } free_register(reg); list_T *const args = tv_list_alloc(1); @@ -202,7 +211,8 @@ err: void set_clipboard(int name, yankreg_T *reg) { - if (!adjust_clipboard_name(&name, false, true)) { + if (mc_replaying() // Multicursor cascade never writes the clipboard. + || !adjust_clipboard_name(&name, false, true)) { return; } diff --git a/src/nvim/mcursor.c b/src/nvim/mcursor.c index 9f31584660..b06eaa94da 100644 --- a/src/nvim/mcursor.c +++ b/src/nvim/mcursor.c @@ -462,8 +462,6 @@ static void mc_cascade(void) return; } } - // Optimization: one clipboard-provider sync for the whole cascade. - start_batch_changes(); McSandbox sb; mc_sandbox_enter(&sb, edits); @@ -489,7 +487,6 @@ static void mc_cascade(void) done: atoms_free(&g_atoms); mc_sandbox_leave(&sb); - end_batch_changes(); mc_cleanup(true); if (handle_get_buffer(sb.bufnr) == curbuf && !curbuf->b_u_synced && curbuf->b_u_newhead != NULL) { @@ -1033,6 +1030,10 @@ static void mc_reg_gather(void) if (gather[i] && kv_size(joined[i]) > 0) { write_reg_contents_ex(MC_REGS[i], joined[i].items, (ssize_t)kv_size(joined[i]), false, kMTLineWise, 0); + if (MC_REGS[i] == '"') { + // If implicit clipboard is enabled (clipboard=unnamed[plus]): write the joined result. + set_clipboard(NUL, get_y_previous()); + } } kv_destroy(joined[i]); } diff --git a/src/nvim/register.c b/src/nvim/register.c index fcbd16be90..c5b0d27e60 100644 --- a/src/nvim/register.c +++ b/src/nvim/register.c @@ -71,6 +71,9 @@ yankreg_T *get_y_register(int reg) return &y_regs[reg]; } +/// What `p` pastes: the last-written register (x: -, yy: 0, "+yy: +, :let @"=…: 0). +/// +/// Exception: with clipboard=unnamed/unnamedplus, `p` tries the provider first. yankreg_T *get_y_previous(void) FUNC_ATTR_PURE { diff --git a/test/functional/editor/mcursor_spec.lua b/test/functional/editor/mcursor_spec.lua index 9a42f457e2..a75c267ddb 100644 --- a/test/functional/editor/mcursor_spec.lua +++ b/test/functional/editor/mcursor_spec.lua @@ -2891,9 +2891,9 @@ describe('multicursor', function() eq({ '', '' }, get_lines()) end) - it("perf: provider syncs once per cascade with 'clipboard'", function() + it('clipboard: implicit clipboard=unnamed[plus], explicit "+', function() n.exec_lua([[ - _G.copies = 0 + _G.copies, _G.pastes = 0, 0 _G.content = {} vim.g.clipboard = { name = 'test', @@ -2905,20 +2905,40 @@ describe('multicursor', function() }, paste = { ['+'] = function() + _G.pastes = _G.pastes + 1 return _G.content end, }, } - vim.o.clipboard = 'unnamedplus' ]]) + local function provider() + return n.exec_lua('return { _G.copies, _G.pastes, _G.content }') + end + + -- Implicit clipboard (clipboard=unnamed[plus]) is ignored during cascade. + -- On multicursor exit, the unnamed (") join is written to the clipboard. + command('set clipboard=unnamedplus') cursors({ 'aa bb', 'cc dd', 'ee ff' }) - local base = n.exec_lua('return _G.copies') feed('dw') eq({ 'bb', 'dd', 'ff' }, get_lines()) - -- One provider sync for the primary's own delete, ONE for the whole - -- cascade (not one per cursor), and the primary's registers win. - eq(base + 2, n.exec_lua('return _G.copies')) - eq({ 'ee ' }, n.exec_lua('return _G.content')) + -- Clipboard provider was updated only by the primary's "dw". Not cascaded. + eq({ 1, 0, { 'ee ' } }, provider()) + feed('p') -- Pastes unnamed reg (not clipboard) per-cursor. + eq({ 'baa b', 'dcc d', 'fee f' }, get_lines()) + eq({ 1, 1, { 'ee ' } }, provider()) + clear_cursors() -- Exit: the joined yank is written to the clipboard. + eq({ 2, 1, { 'aa ', 'cc ', 'ee ', '' } }, provider()) + + -- Explicit clipboard "+". + command('set clipboard=') + cursors({ 'aa', 'bb' }, 'Qj0') + feed('"+yy') -- Explicit "+: writes the primary's yank to the clipboard. + eq({ 3, 1, { 'bb', '' } }, provider()) + feed('"+p') -- Explicit "+: pastes the primary's clipboard at every cursor. + eq({ 'aa', 'bb', 'bb', 'bb' }, get_lines()) + eq({ 3, 2, { 'bb', '' } }, provider()) + clear_cursors() + eq({ 3, 2, { 'bb', '' } }, provider()) end) end)