Merge #41599 from justinmk/mchammer

This commit is contained in:
Justin M. Keyes
2026-09-02 07:46:37 -04:00
committed by GitHub
6 changed files with 71 additions and 25 deletions

View File

@@ -217,6 +217,8 @@ The following new features were added.
• Navigating the |jumplist| with CTRL+O, CTRL+I behaves more intuitively
when deleting buffers, and avoids "invalid buffer" cases. #25461
• |:fclose| command.
• v_Q-default and |v_@-default| repeat a register for each line of a linewise
visual selection.
• Clicking on a tabpage in the tabline with the middle mouse button closes it.
• |:checkhealth| buffer can be opened in a split window using modifiers like
|:vertical|, |:horizontal| and |:botright|.

View File

@@ -327,16 +327,15 @@ q Stops recording.
==============================================================================
Multiple cursors *mcursor* *multicursor*
You can place extra cursors ("multicursors") in a buffer, to repeat operations
at each cursor, as-you-type. This is like a live |macro|, but unlike macros (a
stream of keys without any context), actions (|CmdAtom|) replay "semantically".
You can place extra cursors ("multicursor") in a buffer, to repeat operations
at each cursor as-you-type. Unlike a |macro| (a stream of keys without any
context), multicursor replays actions "semantically" (|CmdAtom|).
- Mappings and Normal-mode operators are applied at each cursor.
- Normal-mode commands, Mappings, and even Macros (|@|) replay at each cursor.
- Insert-mode edits appear at each cursor as you type.
- Motions (|q=|) and Visual-mode sequences replay at each cursor: "viweex"
re-executes the selection there, so the extents are per-cursor (each
cursor's own word, block, …).
- Macros (|@|) replay at each cursor.
- Motions (|q=|) and Visual sequences replay at each cursor: "viweex"
re-executes the selection so the extents are per-cursor (each cursor's own
word, block, …).
- Registers are cursor-local. Each cursor reads/writes its own registers.
The yanks are joined (linewise) when the multicursor session ends.
- Undo is atomic: |u| reverts the aggregate edits from all cursors at once.
@@ -371,8 +370,8 @@ COMMANDS
Q Toggles a multicursor at the current cursor position.
Disables follow-mode |q=|.
With a [count]: places a cursor at every match (|gn|)
of the last search pattern. Example:
[count]Q Places a cursor at every match (|gn|) of the last
search pattern. Example:
1. Search for something ("*", "/pattern<CR>", …).
2. Type "1Q".
@@ -413,6 +412,25 @@ g CTRL-A During a multicursor session, inserts an ascending
[C Jump to the [count]'th previous cursor.
EXAMPLES *mcursor-examples*
Example: a "follow-once" mapping: "q-{motion}" replays {motion} at every
cursor, then disables follow-mode again. >lua
vim.keymap.set('n', 'q-', function()
vim.cmd('normal! 1q=')
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
if ev.data.lhs == 'q-' then
return -- Skip the mapping itself.
end
vim.cmd('normal! 2q=')
return true -- Delete the handler.
end,
})
end)
<
LIMITATIONS *mcursor-limitations*
- Extra cursors on the same line can be lost or misplaced by an edit that

View File

@@ -143,7 +143,7 @@ static const char e_missing_argument_str[]
// they still work when the runtime files can't be found.
static const char *highlight_init_both[] = {
"Cursor guifg=bg guibg=fg",
"Cursor guifg=bg guibg=fg cterm=reverse",
"CursorLineNr gui=bold cterm=bold",
"Ignore guifg=bg ctermfg=0",
"PmenuMatch gui=bold cterm=bold",
@@ -153,7 +153,7 @@ static const char *highlight_init_both[] = {
"TabLineSel guifg=fg guibg=bg gui=bold cterm=nocombine",
"TermCursor gui=reverse cterm=reverse",
"Underlined gui=underline cterm=underline",
"lCursor guifg=bg guibg=fg",
"lCursor guifg=bg guibg=fg cterm=reverse",
// UI
"default link CursorIM Cursor",

View File

@@ -223,8 +223,8 @@ bool mc_replaying(void)
/// position (e.g. "o" on the line above).
/// @param no_undo Transient mark, undo must not restore it (a restored right-gravity mark
/// drifts when undo re-inserts text at its position).
static void mc_point_mark_set(buf_T *buf, uint32_t ns, uint32_t *mark, pos_T pos, bool watched,
bool right_gravity, bool no_undo)
static void mc_mark_set(buf_T *buf, uint32_t ns, uint32_t *mark, pos_T pos, bool watched,
bool right_gravity, bool no_undo)
{
DecorInline decor = DECOR_INLINE_INIT;
if (watched) {
@@ -237,7 +237,7 @@ static void mc_point_mark_set(buf_T *buf, uint32_t ns, uint32_t *mark, pos_T pos
/// Creates or updates the extmark tracking a multicursor position.
static void mc_mark_upd(buf_T *buf, uint32_t *mark, pos_T pos)
{
mc_point_mark_set(buf, mc_ns(), mark, pos, true, true, false);
mc_mark_set(buf, mc_ns(), mark, pos, true, true, false);
}
/// Gets the position tracked by `mark`, adjusted for buffer edits since the mark was set.
@@ -258,7 +258,7 @@ static bool mc_mark_get(buf_T *buf, uint32_t ns, uint32_t mark, pos_T *pos)
/// across a cascade, the preview-apply cursor).
static void mc_track_upd(buf_T *buf, uint32_t *mark, pos_T pos)
{
mc_point_mark_set(buf, mc_session_ns(), mark, pos, false, true, true);
mc_mark_set(buf, mc_session_ns(), mark, pos, false, true, true);
}
/// Enters a replay sandbox, so keys fed (cascaded) per-cursor do not modify pending typeahead nor
@@ -1091,7 +1091,7 @@ void mc_vsel_refresh(void)
}
// Selection-end cursor; the anchor extmark stays put (the eventual replay position).
uint32_t cmark = 0;
mc_point_mark_set(curbuf, mc_vcur_ns(), &cmark, curwin->w_cursor, true, false, true);
mc_mark_set(curbuf, mc_vcur_ns(), &cmark, curwin->w_cursor, true, false, true);
mc_vsel_buf = curbuf->handle;
Visual.active = false;
}
@@ -1231,7 +1231,6 @@ bool mc_follow_toggle(long count0)
} else {
return false;
}
smsg(0, _("multicursor: follow motion %s"), mc_follow_motion ? "on" : "off");
return true;
}
@@ -1345,7 +1344,7 @@ void mc_ns_cleared(buf_T *buf, uint32_t ns_id)
continue;
}
uint32_t mark = 0;
mc_point_mark_set(buf, mc_last_ns(), &mark, ctx->pos, false, true, false);
mc_mark_set(buf, mc_last_ns(), &mark, ctx->pos, false, true, false);
}
mc_dedupe(); // Cleanup.
if (kv_size(mc_cursors) == 0) {

View File

@@ -434,7 +434,7 @@ describe('multicursor', function()
line11 |
line12 |
line13 |
multicursor: ...ow motion off |
|
]])
end)
end)
@@ -2468,10 +2468,10 @@ describe('multicursor', function()
screen:expect({ any = '1×' })
feed('jQ')
screen:expect({ any = '2×' })
command('silent normal! 1q=') -- Follow mode: "=" prefix (silent to avoid the q= message).
feed('l') -- Tickle showcmd redraw.
feed('q=') -- Follow mode: "=" prefix.
feed('l') -- Tickle showcmd redraw.
screen:expect({ any = '=2×' })
command('silent normal! 2q=')
feed('q=') -- Follow mode off.
feed('h') -- Tickle showcmd redraw.
clear_cursors() -- cleared with the cursors
feed('<Esc>') -- the indicator refreshes on the next command
@@ -2492,13 +2492,37 @@ describe('multicursor', function()
screen:expect({ any = '1×' })
feed('jQ')
screen:expect({ any = '2×' })
command('silent normal! 1q=') -- Follow mode: "=" prefix (silent to avoid the q= message).
feed('q=') -- Follow mode: "=" prefix.
feed('l') -- Tickle showcmd redraw.
screen:expect({ any = '=2×' })
end)
end)
describe('workflows', function()
it('follow-once mapping', function()
n.exec_lua([[
vim.keymap.set('n', 'q-', function()
vim.cmd('normal! 1q=')
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
if ev.data.lhs == 'q-' then
return
end
vim.cmd('normal! 2q=')
return true
end,
})
end)
]])
cursors({ ' aaa', ' bbb', ' ccc' })
feed('q-')
feed('^') -- Cascades: every cursor moves to the first non-blank.
eq({ { 0, 2 }, { 1, 2 } }, anchors())
feed('l') -- Follow already ended: primary only.
eq({ { 0, 2 }, { 1, 2 } }, anchors())
eq(4, fn.col('.'))
end)
it('split visual selection into line cursors', function()
-- {Visual}Q
fn.setline(1, { 'aaaa', 'bbbb', 'cc', 'dddd' })

View File

@@ -242,7 +242,10 @@ describe('highlight defaults', function()
it('Cursor after `:hi clear|syntax reset` #6508', function()
command('highlight clear|syntax reset')
eq('guifg=bg guibg=fg', eval([[matchstr(execute('hi Cursor'), '\v(gui|cterm).*$')]]))
eq(
'cterm=reverse guifg=bg guibg=fg',
eval([[matchstr(execute('hi Cursor'), '\v(gui|cterm).*$')]])
)
end)
it('Whitespace highlight', function()