fix(multicursor): cascade mapping atoms with insert entry #41714

Problem:
Pending mapping atoms prevent live insert-cascading (`nnoremap i ^i`).
Flushing them at insert-session start (3a02a39957) adds a separate
cascade boundary, which muddles the architecture.

Solution:
Let the first insert-span cascade pending mapping atoms on insert-entry.
This is an alternative fix for #41692.

Composite children (`.atoms`) BEFORE:

    { { 'motion', '^' }, { 'insert', '1iX<Esc>' } }

Composite children (`.atoms`) AFTER:

    { { 'motion', '^' }, { 'insert', '1i<Esc>' }, { 'insert', 'iX<Esc>' } }
This commit is contained in:
Justin M. Keyes
2026-09-05 09:53:41 -04:00
committed by GitHub
parent 739a44753d
commit 0c642196b9
5 changed files with 25 additions and 19 deletions

View File

@@ -1,9 +1,10 @@
// Context = "full app state" abstraction. (Note: it's named "Context" to disambiguate with state.c
// which is about the event-loop state-machine, not "total program state".)
// Context = "full app state" abstraction. So named to disambiguate state.c, which is about the
// event-loop state-machine, not "total program state".
//
// Unified interface of:
// + shada
// + CtxSwitch/ctx_switch (FKA: aucmd_prepbuf, switch_win, win_execute_T)
// + TODO: save_state_T/save_current_state
// + TODO: sessions
// + TODO: undo save/restore (for cmdpreview, multicursor)
// + TODO: TRY_WRAP ?
@@ -11,7 +12,7 @@
// Related:
// - vim.with()
// - switch_option_context(), restore_option_context()
// - McSandbox: input-replay guard. Sibling axis to ctx_switch() and ctx_save().
// - McSandbox/mc_sandbox_enter: input-replay guard. Sibling axis to ctx_switch() and ctx_save().
#include <assert.h>
#include <stdbool.h>

View File

@@ -7092,7 +7092,7 @@ void update_topline_cursor(void)
update_curswant();
}
/// Save the current State and go to Normal mode.
/// Save the current State (editor-mode) and go to Normal mode.
void save_current_state(save_state_T *sst)
FUNC_ATTR_NONNULL_ALL
{

View File

@@ -493,7 +493,7 @@ EXTERN int inhibit_delete_count INIT( = 0);
/// Encoding used when 'fencs' is set to "default"
EXTERN char *fenc_default INIT( = NULL);
/// "State" is the main state of Vim.
/// "State" is the main state (editor-mode) of Vim.
/// There are other variables that modify the state:
/// Visual_mode: When State is MODE_NORMAL or MODE_INSERT.
/// finish_op : When State is MODE_NORMAL, after typing the operator and

View File

@@ -595,10 +595,6 @@ void mc_ins_cascade_start(bool cascade, varnumber_T tick)
return;
}
mc_ins_joined = false;
if (cascade && mc_buf_has_cursors(curbuf) && kv_size(g_atoms) > 0) {
// Cascade now if a mapping queued atoms before entering Insert ("nnoremap i ^i").
mc_cascade();
}
mc_ins_span.active = cascade && mc_buf_has_cursors(curbuf);
mc_ins_span.first = true;
mc_ins_span.done_len = 0;
@@ -640,11 +636,13 @@ static void mc_ins_restore_state(const McInsSaved *saved)
curbuf->b_last_changedtick_i = saved->last_changedtick_i;
}
/// Pushes one span and immediately cascades it. Takes ownership of `keys` and `text`.
/// Pushes one span and immediately cascades it, plus any pending mapping atoms.
/// Takes ownership of `keys` and `text`.
static void mc_ins_span_push(char *keys, char *text)
{
mc_ins_span.first = false;
// If all cursors disappear mid-session (e.g. by dedupe), emit but don't cascade.
// If all cursors disappear mid-session (e.g. dedupe), emit but don't cascade.
bool cascade = mc_buf_has_cursors(curbuf);
atom_push_raw(cascade, &(CmdAtom){
.type = kAInsertSpan,
@@ -655,6 +653,7 @@ static void mc_ins_span_push(char *keys, char *text)
if (!cascade) {
return;
}
McInsSaved saved = mc_ins_save_state();
block_autocmds(); // The span replay would fire InsertEnter/InsertLeave on every key.
mc_cascade();
@@ -823,7 +822,7 @@ void mc_ins_cascade_restart(void)
void mc_ins_cascade(void)
{
if (!mc_ins_span.active || mc_replaying() || !(State & MODE_INSERT)
|| !mc_buf_has_cursors(curbuf) || kv_size(g_atoms) != 0) {
|| !mc_buf_has_cursors(curbuf)) {
return;
}
String ins = redo_keys(NULL);

View File

@@ -1215,14 +1215,10 @@ describe('multicursor', function()
-- Also when a Normal-mode mapping moves before entering Insert. #41605
clear_cursors()
cursors({ 'aaaa', 'bbbb', 'cccc' }, 'llQjQj')
eq({ { 0, 2 }, { 1, 2 } }, anchors())
eq({ 3, 2 }, api.nvim_win_get_cursor(0))
command('nnoremap i ^i')
screen:expect([[
aa{17:a}a |
bb{17:b}b |
cc^cc |
{1:~ }|*2
|
]])
atoms_start()
feed('iX')
screen:expect([[
X{17:a}aaa |
@@ -1233,6 +1229,16 @@ describe('multicursor', function()
]])
feed('<Esc>')
eq({ 'Xaaaa', 'Xbbbb', 'Xcccc' }, get_lines())
local ev = atom_last()
eq(
{ type = 'mapping', lhs = k('iX<Esc>'), changed = true },
t_atom.pick(atom_last(), 'type', 'lhs', 'changed')
)
local children = {}
for _, child in ipairs(ev.atoms) do
children[#children + 1] = { child.type, child.keys }
end
eq({ { 'motion', '^' }, { 'insert', k('1i<Esc>') }, { 'insert', k('iX<Esc>') } }, children)
end)
end)