feat(multicursor): MC HAMMER #41587

Other (squashed) commits:

fix(tui): emit ui_send output atomically with the frame

Problem:
tui_ui_send() writes directly to the TTY, bypassing the output buffer.
Sequences sent via nvim_ui_send() (e.g. kitty multiple-cursors, or
visual-dot-repeat) always arrive in a separate TTY write from the frame
they were computed for. This manifests as "tearing", or e.g. in the case
of multicursor the terminal renders text with stale cursor overlays.

Solution:
- tui_ui_send(): while a frame is being assembled (pending invalid
  regions or buffered output), buffer instead of writing directly.
  - Out-of-frame sends (tty queries, clear-on-disable) still write
    immediately.
- mcursor.lua: emit the terminal-cursor update at the end of the redraw
  cycle (`on_end`, when screen positions are final) instead of
  vim.schedule().
This commit is contained in:
Justin M. Keyes
2026-09-01 11:17:22 -04:00
committed by GitHub
parent 9ebf9b1017
commit 9a29622b54
63 changed files with 5093 additions and 347 deletions

View File

@@ -124,7 +124,7 @@ describe('CmdAtom', function()
it('Lua-callback mapping (e.g. "]q" default)', function()
-- Same shape as the "]q" default mapping: a Lua callback with no
-- replayable keys. Still a user action: it publishes with an empty
-- replayable keys. Still a user action: it emits with an empty
-- replay payload.
n.exec_lua([[
vim.keymap.set('n', ']q', function()
@@ -199,7 +199,7 @@ describe('CmdAtom', function()
end)
it('motions, search, Ex emit without an edit', function()
-- Emission is not tied to editing: every user action publishes, so
-- Emission is not tied to editing: every user action emits, so
-- plugins can observe all activity.
fn.setline(1, { 'alpha beta', 'gamma delta' })
feed('gg0')
@@ -409,6 +409,29 @@ describe('CmdAtom', function()
feed('2G&3G2&')
eq({ 'red a', 'red b', 'red c', 'red d' }, get_lines())
eq({ type = 'excmd', lhs = '2&', keys = ':.,.+1s\n' }, pick(atom_last(), 'type', 'lhs', 'keys'))
-- Reg "." put re-inserts through edit(), and a mapping's composite labels it.
api.nvim_buf_set_lines(0, 0, -1, true, { 'one' })
feed('ggifoo<Esc>')
feed('$".p')
eq('fooonefoo', fn.getline(1))
eq(
{ type = 'insert', lhs = k('1afoo<Esc>'), keys = k('1afoo<Esc>') },
pick(atom_last(), 'type', 'lhs', 'keys')
)
command('nnoremap ,p ".p')
feed(',p')
eq(
{ type = 'insert', lhs = ',p', keys = k('1afoo<Esc>') },
pick(atom_last(), 'type', 'lhs', 'keys')
)
-- Typed ":put ." emits typed cmdline payload, not the internal ":put _" translation.
feed(':put .<CR>')
eq('foo', fn.getline(2))
eq(
{ type = 'excmd', lhs = ':put .\n', keys = ':put .\n', text = 'put .' },
pick(atom_last(), 'type', 'lhs', 'keys', 'text')
)
end)
it('mapping that enters :terminal mode', function()
@@ -468,11 +491,6 @@ describe('CmdAtom', function()
local evs = take()
eq(1, #evs)
eq('@q', evs[1].lhs)
-- Same for "Q" (replay the last recorded register).
feed('Q')
evs = take()
eq(1, #evs)
eq({ lhs = '@q', keys = 'dl' }, pick(evs[1], 'lhs', 'keys'))
-- Programmatic / replayed input must NOT leak any atom.
fresh()
@@ -1444,6 +1462,12 @@ describe('CmdAtom', function()
end,
})
vim.keymap.set('n', '.', function()
-- Multicursors: degrade to builtin ".", which repeats at each cursor.
local mc = vim.api.nvim_create_namespace('nvim.multicursor')
if #vim.api.nvim_buf_get_extmarks(0, mc, 0, -1, { limit = 1 }) > 0 then
vim.api.nvim_feedkeys('.', 'n', false)
return
end
vim.schedule(function()
if last then
vim.api.nvim_feedkeys(last.keys or last.lhs, last.keys and 'n' or 'm', false)
@@ -1491,6 +1515,17 @@ describe('CmdAtom', function()
retry(nil, 1000, function()
eq('cd', fn.getline(1))
end)
-- While multicursor is active, the "." mapping degrades to builtin "." so it cascades.
api.nvim_buf_set_lines(0, 0, -1, true, { 'aaa', 'bbb', 'ccc' })
feed('gg0QjQj')
feed('x')
n.poke_eventloop()
eq({ 'aa', 'bb', 'cc' }, api.nvim_buf_get_lines(0, 0, -1, true))
feed('.')
retry(nil, 1000, function()
eq({ 'a', 'b', 'c' }, api.nvim_buf_get_lines(0, 0, -1, true))
end)
end)
it('"," repeats the last motion atom', function()