diff --git a/runtime/doc/news-0.10.txt b/runtime/doc/news-0.10.txt index df49349df4..4801ebfb98 100644 --- a/runtime/doc/news-0.10.txt +++ b/runtime/doc/news-0.10.txt @@ -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|. diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt index 5c5a127277..90d4d5a0d8 100644 --- a/runtime/doc/repeat.txt +++ b/runtime/doc/repeat.txt @@ -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", …). 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 diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c index 4f0da40391..4f0b976a7e 100644 --- a/src/nvim/highlight_group.c +++ b/src/nvim/highlight_group.c @@ -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", diff --git a/src/nvim/mcursor.c b/src/nvim/mcursor.c index 5e8f0f1734..a409c3b1f1 100644 --- a/src/nvim/mcursor.c +++ b/src/nvim/mcursor.c @@ -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) { diff --git a/test/functional/editor/mcursor_spec.lua b/test/functional/editor/mcursor_spec.lua index 06ed5775bd..e5ab779ae5 100644 --- a/test/functional/editor/mcursor_spec.lua +++ b/test/functional/editor/mcursor_spec.lua @@ -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('') -- 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' }) diff --git a/test/functional/ui/highlight_spec.lua b/test/functional/ui/highlight_spec.lua index 465923fce5..9402388949 100644 --- a/test/functional/ui/highlight_spec.lua +++ b/test/functional/ui/highlight_spec.lua @@ -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()