mirror of
https://github.com/neovim/neovim.git
synced 2026-09-12 09:01:02 +00:00
fix(multicursor): clipboard=unnamed, explicit "+yy #41804
Problem: - With `clipboard=unnamed[plus]`, per-cursor yanks are joined into '"' on exit, but not the clipboard. - Multicursor `"+yy` calls the clipboard provider per-cursor, behaving as "last wins". Solution: - Never cascade to/from the clipboard provider. - Implicit clipboard (`clipboard=unnamed[plus]`) uses the cursor-local unnamed register, so the cascade needs no `start_batch_changes()`. On exit, the clipboard gets the same joined result as the `"` register. - Operations that reference the explicit clipboard registers `"+` / `"*` while multicursor is active, use the cached (primary) clipboard value.
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user