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.
This commit is contained in:
Justin M. Keyes
2026-09-03 08:44:22 -04:00
committed by GitHub
parent 73923b0dd8
commit e1cde28ba2
2 changed files with 24 additions and 12 deletions

View File

@@ -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();

View File

@@ -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 <Esc> 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('<Esc>')
-- 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('<Esc>')
-- 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('<Esc>')
-- 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('<Esc>')
end)
end)