From e1cde28ba28544a28ce18cfaf272e09204010460 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 3 Sep 2026 08:44:22 -0400 Subject: [PATCH] fix(multicursor): live-mirror Visual-mode operator mapping #41655 Problem: Insert entered by a Visual-mode operator mapping (`xnoremap c c`) does not live-mirror at cursors. Solution: Decide `typed` from the Visual session's own provenance (`vatom`, which spans the selection and knows whether it was user input) instead of only negating it. --- src/nvim/input_cmdatom.c | 14 +++++++++----- test/functional/editor/mcursor_spec.lua | 22 +++++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/nvim/input_cmdatom.c b/src/nvim/input_cmdatom.c index c9e05a1587..0b2af19e1d 100644 --- a/src/nvim/input_cmdatom.c +++ b/src/nvim/input_cmdatom.c @@ -77,7 +77,7 @@ static struct { ///< ("f(" + mapped key in one batch). 0: none. } composite; -/// State of a Visual composite atom. +/// State of a Visual session atom. typedef enum { kVatomNone = 0, ///< No pending Visual atom. @@ -88,7 +88,10 @@ typedef enum { kVatomVoid = 4, ///< Not replayable: tainted/poisoned (by mouse, gv, …). But may emit CmdAtom. } VatomState; -/// Accumulating Visual composite: the full Visual keysequence (selection keys + operator). +/// Accumulating Visual session atom: the full Visual keysequence (selection keys + operator). +/// +/// NOTE: Visual session is not modeled as `composite` bc they may overlap (not clean nesting): +/// a mapping can open before `v` and end mid-selection ("nmap X vjj" followed by "d"). static struct { CmdAtomVec atoms; ///< Accumulated subatoms. A void session collects them as the `lhs` label. VatomState state; @@ -1147,9 +1150,10 @@ InsSession atom_ins_start(int cmd, long count, VisualIns vis, bool vblock) : cur_frame != NULL ? cur_frame->origin : atom_origin(), }; if (vis != kVInsNone && !mc_replaying()) { - if (vis == kVInsKeys && !(atom_visual_replayable() && (vatom.state & kVatomTyped))) { - // The selection came from fed keys (":norm", scheduled feedkeys). - session.typed = false; + if (vis == kVInsKeys) { + // Internal (non-user) keys (":norm", scheduled feedkeys) are not user-input. + // But a Visual-mode operator mapping ("xnoremap c c") is user-input. #41605 + session.typed = atom_visual_replayable() && (vatom.state & kVatomTyped) != 0; } // The selection is consumed: already in the redo body. Also clears selection display. atom_visual_reset(); diff --git a/test/functional/editor/mcursor_spec.lua b/test/functional/editor/mcursor_spec.lua index 9ffc152be9..2de723a572 100644 --- a/test/functional/editor/mcursor_spec.lua +++ b/test/functional/editor/mcursor_spec.lua @@ -1132,7 +1132,7 @@ describe('multicursor', function() ]]) end) - it('typed text appears at cursors before leaving insert-mode', function() + it('typed text live-mirrors at cursors', function() local screen = Screen.new(30, 6) cursors({ 'aaa', 'bbb', 'ccc' }) -- Still in insert mode (no yet): the text already cascaded; each cursor displays @@ -1155,8 +1155,7 @@ describe('multicursor', function() {1:~ }|*2 | ]]) - -- Entering insert mode displays each cursor at its insertion point - -- right away, BEFORE any text is typed ("a": one char to the right). + -- Entering insert mode ("a") displays each cursor at its insertion-point immediately. feed('a') screen:expect([[ XY{17:a}aa | @@ -1166,8 +1165,7 @@ describe('multicursor', function() {5:-- INSERT --} | ]]) feed('') - -- An operator entry ("ciw") live-mirrors the same way: the entry - -- replay changes each cursor's OWN word, still in insert mode. + -- Operator entry ("ciw") live-mirrors the per-cursor change, still in insert mode. feed('0ciwZ') screen:expect([[ Z{17: } | @@ -1177,8 +1175,7 @@ describe('multicursor', function() {5:-- INSERT --} | ]]) feed('') - -- A Visual-entered change ("viwc") live-mirrors too: the entry replay - -- re-executes the selection at each cursor. + -- Visual-change entry ("viwc") live-mirrors the selection at each cursor. feed('viwcW') screen:expect([[ W{17: } | @@ -1188,6 +1185,17 @@ describe('multicursor', function() {5:-- INSERT --} | ]]) feed('') + -- Also when "c" is a Visual-mode operator mapping. #41605 + command('xnoremap c "_c') + feed('viwcM') + screen:expect([[ + M{17: } | + M{17: } | + M^ | + {1:~ }|*2 + {5:-- INSERT --} | + ]]) + feed('') end) end)