fix(multicursor): abandoned Visual selection does not move cursors #41670

Problem:
"v{motion}<Esc>" moves the primary cursor to the selection end, but the
other cursors snap back to their anchors unless follow-mode ("q=") is
on. The selection preview showed them at the selection end.

Solution:
Replay the abandoned selection at every cursor, not only in follow-mode.
An abandoned typed selection now always emits a "visual" CmdAtom.
This commit is contained in:
Justin M. Keyes
2026-09-03 15:20:30 -04:00
committed by GitHub
parent ce8a897f98
commit 5209695703
3 changed files with 24 additions and 23 deletions

View File

@@ -1310,9 +1310,9 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old)
vatom.state |= kVatomVoid;
}
} else if (old->visual.active) {
if (user && old->follow && atom_visual_replayable() && kv_size(vatom.atoms) > 0) {
// Follow-motion ("q="): a selection abandoned without an operator (<Esc>, "v" toggle) still
// moved the primary cursor to the selection end: replay it at every cursor.
if (user && atom_visual_replayable() && kv_size(vatom.atoms) > 0) {
// Cursor moved to selection-end after ESC/"v"-toggle. Even though this is technically
// a "motion", it's more intuitive to always replay it? #41631
char suffix[2] = { ESC, NUL };
atom_visual_end_suffix(xstrdup(suffix), NULL, false);
} else {

View File

@@ -849,11 +849,15 @@ describe('CmdAtom', function()
feed(',D')
eq(before, #atoms())
atom('w', 'dw', ',Dw') -- The supplied motion finishes the operation
-- Same for a mapping that opens Visual mode: "<Esc>" abandons the selection and closes it.
-- Same for a mapping that opens Visual mode: ESC closes the selection (cursor stays on the
-- selection end, where it moved during the Visual sequence).
feed(',V')
eq(before + 1, #atoms())
feed('<Esc>')
eq({ type = 'mapping', lhs = k(',V<Esc>') }, pick(atom_last(), 'type', 'lhs', 'keys'))
eq(
{ type = 'visual', lhs = k(',V<Esc>'), keys = k('v<Esc>') },
pick(atom_last(), 'type', 'lhs', 'keys')
)
-- A mapping whose trailing prefix is completed by TYPED keys ("," + "w")
-- ends where its own keys stop: each `lhs` owns only what it produced.
command('set notimeout')

View File

@@ -1533,10 +1533,11 @@ describe('multicursor', function()
{1:~ }|*2
{5:-- VISUAL --} |
]])
-- <Esc>: cursors move to the selection-end, like the primary.
feed('<Esc>')
screen:expect([[
{17:l}ongword x |
{17:a}b y |
longword {17:x} |
ab {17:y} |
medium ^z |
{1:~ }|*2
|
@@ -1557,7 +1558,8 @@ describe('multicursor', function()
{5:-- VISUAL LINE --} |
]])
feed('<Esc>')
feed('3G0l')
clear_cursors()
cursors({ 'aaaa', 'bbbb', 'cccc', 'dddd' }, 'Q2jl')
feed('<C-v>jl') -- blockwise: primary (3,1)-(4,2), fake (1,0)-(2,1)
screen:expect([[
{17:aa}aa |
@@ -1605,11 +1607,19 @@ describe('multicursor', function()
})
end)
it('<Esc> discards the pending visual atom', function()
it('ESC moves cursors to selection-end', function()
cursors({ 'abc', 'def' }, 'Qj')
feed('viw<Esc>')
feed('x') -- cascades normally; no stray visual replay
eq({ 'bc', 'de' }, get_lines())
eq({ 'ab', 'de' }, get_lines())
-- Without follow-mode a plain motion ("0") stays primary-only.
-- But the selection itself moves every cursor to its selection-end.
clear_cursors()
cursors({ 'aaa bbb ccc', 'ddd eee fff', 'ggg hhh iii' }, '4lQjQj')
feed('viw<Esc>x')
eq({ 'aaa bb ccc', 'ddd ee fff', 'ggg hh iii' }, get_lines())
feed('0viwe<Esc>x')
eq({ 'aaa bb cc', 'ddd ee ff', 'ggg h iii' }, get_lines())
end)
it('cursor displays at each selection end; o swaps it', function()
@@ -1807,19 +1817,6 @@ describe('multicursor', function()
eq({ '1', 'a2', 'b1', '2' }, get_lines())
end)
it('abandoned visual selection moves cursors to their selection ends', function()
cursors({ 'aaa bbb ccc', 'ddd eee fff', 'ggg hhh iii' }, '4lQjQj')
feed('q=')
feed('viw<Esc>') -- Selection end: the last char of each cursor's word.
feed('x')
eq({ 'aaa bb ccc', 'ddd ee fff', 'ggg hh iii' }, get_lines())
-- No follow: <Esc> discards, other cursors stay; "x" cascades.
feed('q=')
feed('0viw<Esc>')
feed('x')
eq({ 'aaa bbccc', 'ddd eefff', 'gg hh iii' }, get_lines())
end)
it('per-cursor curswant is kept over short lines', function()
fn.setline(1, { 'ABCDEF', 'xy', 'GHIJKL', 'MNOPQR', 'zw', 'STUVWX' })
feed('gg04l') -- Cursor at (1,4).