mirror of
https://github.com/neovim/neovim.git
synced 2026-11-06 05:16:52 +00:00
fix(multicursor): insert-mode Lua/API edit dropped on ESC #42312
Problem:
With multicursors, an Insert-mode edit by a Lua/`<Cmd>` mapping or API
(nvim_put(), …) is live-previewed at the other cursors, but dropped at
ESC: the cascade replays _redo_, which does not contain such edits.
Solution:
When `<Cmd>`/Lua/RPC changes the buffer during Insert-mode, sync the
previews and restart the capture (like a non-captured cursor-move). The
edit stays as text.
- The mapping runs only at the primary. Its text is mirrored as-is
(similar to insert-completion). Re-running it per cursor would require
a "partial-remap" span replay, and breaks single-session plugins
(vim.snippet).
- Not during completion; redo already captures the pre-inserted
candidate on accept.
- Text typed before the edit is kept as mirrored, not replayed, so
per-cursor effects ('textwidth' wrapping, …) don't apply to it.
Known issues:
- "o"/"O"/"cc" with 'autoindent' followed by the mapping: the per-cursor
insert-entry replay is still pending, so the edit is lost.
This commit is contained in:
1 parent
0f17fab619
commit
4bfbad6e91
4 files changed
+63
-7
No files matched your search
@@ -511,6 +511,8 @@ 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).
|
||||
- During Insert-mode, edits by a <Cmd>/Lua/API are not replayed per-cursor;
|
||||
the primary text is mirrored as-is (similar to insert-completion).
|
||||
- Cursor is removed if its Insert-entering operation fails. E.g. "ct;" on
|
||||
a line without ";", "cgn" with no match.
|
||||
- Explicit clipboard registers (`"*yy`, `"+yy`) are global, not per-cursor. On
|
||||
|
||||
+7
-4
@@ -986,14 +986,17 @@ static int insert_handle_key(InsertState *s)
|
||||
pum_want.active = false;
|
||||
}
|
||||
|
||||
if (curbuf->b_u_synced
|
||||
|| (bufref_valid(&save_curbuf)
|
||||
&& curbuf == save_curbuf.br_buf
|
||||
&& tick != buf_get_changedtick(curbuf))) {
|
||||
const bool changed = bufref_valid(&save_curbuf) && curbuf == save_curbuf.br_buf
|
||||
&& tick != buf_get_changedtick(curbuf);
|
||||
if (curbuf->b_u_synced || changed) {
|
||||
// The K_EVENT, K_COMMAND, or K_LUA synced undo or changed this buffer.
|
||||
// Save the cursor line before the next typed edit.
|
||||
Ins.need_undo = true;
|
||||
}
|
||||
if (changed && !ins_compl_active()) { // Multicursor: keep the edit as preview text.
|
||||
mc_ins_cascade();
|
||||
mc_ins_cascade_restart();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -103,6 +103,7 @@ typedef struct {
|
||||
/// The boundary:
|
||||
/// - literal text is previewed
|
||||
/// - non-literal keys (mc_ins_keys_nonliteral) have per-cursor effects, so flush (early commit).
|
||||
/// - non-captured cursor-moves and edits keep the previews (see mc_ins_cascade_restart).
|
||||
static struct {
|
||||
CmdOrigin origin; ///< State at session start: the session's atoms diff against it.
|
||||
kvec_t(uint32_t) regions; ///< Per-cursor preview regions (`mc_session_ns`).
|
||||
@@ -792,8 +793,9 @@ static bool mc_ins_keys_nonliteral(const char *keys, size_t len)
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Capture restarted mid insert-session (stop_arrow(), after a non-captured cursor-move: mouse,
|
||||
/// <PageUp>, …). The previews stay; rebase and continue the insert-cascade.
|
||||
/// Capture restarted mid insert-session, after a non-captured cursor-move (stop_arrow(): mouse,
|
||||
/// <PageUp>, …) or edit (<Cmd>, Lua mapping, RPC, …). The previews stay; rebase and continue the
|
||||
/// insert-cascade.
|
||||
void mc_ins_cascade_restart(void)
|
||||
{
|
||||
if (!mc_ins_span.active || mc_replaying() || !(State & MODE_INSERT)
|
||||
|
||||
@@ -385,12 +385,18 @@ describe('multicursor', function()
|
||||
local screen = Screen.new(30, 5)
|
||||
cursors({ 'aa bb cc', 'dd ee' }, '')
|
||||
feed('zqw')
|
||||
eq({ 1, 0 }, api.nvim_win_get_cursor(0))
|
||||
screen:expect({
|
||||
grid = [[
|
||||
{17:^a}a {17:b}b {17:c}c |
|
||||
{17:d}d {17:ee} |
|
||||
{1:~ }|*2
|
||||
|
|
||||
]],
|
||||
condition = function()
|
||||
eq('normal', screen.mode) -- Not "operator".
|
||||
end,
|
||||
})
|
||||
eq({ 1, 0 }, api.nvim_win_get_cursor(0))
|
||||
-- ("w" on the last word stops at its end.)
|
||||
eq({ { 0, 0 }, { 0, 3 }, { 0, 6 }, { 1, 0 }, { 1, 3 }, { 1, 4 } }, anchors())
|
||||
-- A count applies to the motion ("2zqw" is "zq2w": every 2nd word).
|
||||
@@ -1887,6 +1893,49 @@ describe('multicursor', function()
|
||||
end
|
||||
end)
|
||||
|
||||
it('edit not captured by redo (Lua/RPC) stays as previewed #42205', function()
|
||||
n.exec_lua(function()
|
||||
vim.keymap.set('i', '<C-j>', function()
|
||||
vim.api.nvim_put({ 'X' }, 'c', false, true)
|
||||
end)
|
||||
end)
|
||||
cursors({ 'foo', 'bar' }, 'QjQ')
|
||||
feed('A<C-j>')
|
||||
eq({ 'fooX', 'barX' }, get_lines())
|
||||
feed('<Esc>')
|
||||
eq({ 'fooX', 'barX' }, get_lines())
|
||||
-- Cursors land on the last-inserted char, like the primary.
|
||||
eq({ { 0, 3 }, { 1, 3 } }, anchors())
|
||||
eq({ 2, 3 }, api.nvim_win_get_cursor(0))
|
||||
-- Typed text around the edits; RPC edit (K_EVENT).
|
||||
feed('a1<C-j>2')
|
||||
api.nvim_put({ 'Y' }, 'c', false, true)
|
||||
feed('3<Esc>')
|
||||
eq({ 'fooX1X2Y3', 'barX1X2Y3' }, get_lines())
|
||||
feed('u')
|
||||
eq({ 'fooX', 'barX' }, get_lines())
|
||||
|
||||
-- Typed text after the edit still replays ('textwidth' wraps per cursor), non-literal keys
|
||||
-- after it flush, and a session ended by the mapping (:stopinsert) or CTRL-C keeps it.
|
||||
command('set textwidth=12')
|
||||
command('imap <C-k> <C-j><Cmd>stopinsert<CR>')
|
||||
for _, case in ipairs({
|
||||
{ 'Aab<C-j>cd ee ff<Esc>', { 'fooabXcd ee', 'ff', 'barabXcd ee', 'ff' } },
|
||||
{ 'A<C-j><Left>Y<BS><Esc>', { 'fooX', 'barX' } },
|
||||
{ 'A<C-k>', { 'fooX', 'barX' } },
|
||||
{ 'A<C-j>', { 'fooX', 'barX' }, '<C-c>' },
|
||||
}) do
|
||||
clear_cursors()
|
||||
cursors({ 'foo', 'bar' }, 'QjQ')
|
||||
feed(case[1])
|
||||
if case[3] then
|
||||
n.poke_eventloop() -- Mapping runs before CTRL-C interrupts.
|
||||
feed(case[3])
|
||||
end
|
||||
eq(case[2], get_lines(), case[1])
|
||||
end
|
||||
end)
|
||||
|
||||
it('mapping ending in a Normal-mode "tail", cascades #41864', function()
|
||||
command('inoremap <Esc> <Esc>l') -- the "<Esc>" leaves a Normal-mode tail ("l")
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'echo hi', 'echo bye' })
|
||||
|
||||
Reference in new issue
Block a user