Repeating commands
Rustvim :help pages, generated
from source
using the tree-sitter-vimdoc parser.
Chapter 26 of the user manual introduces repeating usr_26.txt.
Single repeat
Repeat last change, with count replaced with [count].
Also repeat a yank command, when the 'y' flag is
included in 'cpoptions'. Does not repeat a
command-line command.
@:@:
Repeat last command-line [count] times.
Simple changes (edits) can be repeated with the "." command. Without a count,
the count of the last change is used. If you enter a count, it will replace
the last one. v:count and v:count1 will be set.
If the last change included a specification of a numbered register, the
register number will be incremented. See redo-register for an example how
to use this.
Repeating an insert ins-repeat re-executes the whole session, including
non-jump cursor-moves: "iab<Left>c<Esc>." produces "acb", not "c". Note that
jumps (mouse, i_<PageUp>, …) split an insert session.
Repeating a Visual-mode command re-executes the captured keysequence,
selection included. See visual-repeat.
To repeat non-changes, see motion-repeat. To repeat multiple actions, see
cmdatom-macro and complex-repeat.
Semantic repeat
Every user action emits a CmdAtom event. This avoids the need for plugins to
"announce" the repeatable unit, thus plugins like vim-repeat aren't needed.
CmdAtom.lhs is the high-level user input collected during an action,
including getchar() input. This is signficant: it reflects the semantic
intent. CmdAtom.keys reveals the low-level internal commands that were
produced.
INPUT (lhs) | RESOLUTION (keys) | |
|---|---|---|
| dw | "dw" | Not translated. |
| zfa{ | "zfa{" | Not an edit (changed=false). |
| x | "dl" | Translated builtin. |
| ,d | "dl" | Mapping :nnoremap ,d x. |
| @q | "dl" | Macro @q = "x". |
<F6> | "dlw" | Composite :nnoremap <F6> xw: type='mapping', subatoms "dl" and "w" concat to the composite keys. |
| ,Dw | "dw" | Incomplete mapping: ended mid- operation (:nnoremap ,D d), captures its continuation ("w"). |
| ds) | ":call …<NL>)" | "Payload" mapping (e.g. vim-surround "ds'" reads getchar()). |
| ]q | nil | Lua mapping. |
| V<C-E>d | "" | Unreplayable: viewport-dependent Visual sequence; lhs is only a hint/label. |
To repeat an atom, feed
keys (mode "n"), or lhs (mode "m") if keys=nil.
local function replay(a)
vim.api.nvim_feedkeys(a.keys or a.lhs, a.keys and 'n' or 'm', false)
end
Example: This "," mapping repeats the last motion ("zj", "3w", "fx",
"/pat<CR>", "]c", any mapping/command that sets
moved=true).
local last ---@type vim.event.cmdatom.data?
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
-- Skip edits, and the "," mapping itself.
local motion = ev.data.moved or ev.match == 'motion'
if motion and not (ev.data.changed or ev.data.lhs == ',') then
last = ev.data
end
end,
})
vim.keymap.set('n', ',', function()
-- CmdAtom is deferred; schedule the replay, in case "," follows a motion.
vim.schedule(function()
if last then
vim.api.nvim_feedkeys(last.keys or last.lhs, last.keys and 'n' or 'm', false)
end
end)
end)
Example: This "." mapping repeats ANY edit (except undo/redo), including
operations provided by plugins, without the need for "announcement" via
vim-repeat or similar.
local last ---@type vim.event.cmdatom.data?
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
local is_redo_or_undo = ev.data.changed and (ev.data.undoseq or 0) <= (vim.b[ev.buf].maxseq or 0)
vim.b[ev.buf].maxseq = math.max(vim.b[ev.buf].maxseq or 0, ev.data.undoseq or 0)
if ev.data.changed and not is_redo_or_undo and ev.data.lhs ~= '.' then
last = ev.data
end
end,
})
vim.keymap.set('n', '.', function()
-- Multicursors: degrade to builtin "." (cascades).
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
-- CmdAtom is deferred; schedule the replay, in case "." follows an edit.
vim.schedule(function()
if last then
vim.api.nvim_feedkeys(last.keys or last.lhs, last.keys and 'n' or 'm', false)
end
end)
end)
Example: This "[count]<Space>" mapping saves the last [count] atoms as an
editable macro: "2<Space>" opens the cmdwin with the last [count] atoms
listed (you can delete/edit steps if you want),
<Enter> saves it. "<Space>"
without a count replays the macro.
-- Track the last 20 atoms.
local atom_ring = {} ---@type vim.event.cmdatom.data[]
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
-- Skip this mapping itself, and cmdwin edits.
if ev.data.lhs ~= ' ' and vim.fn.getcmdwintype() == '' then
atom_ring[#atom_ring + 1] = ev.data
if #atom_ring > 20 then
table.remove(atom_ring, 1)
end
end
end,
})
-- [count]<space> shows a cmdwin where the user can edit/save the last [count] atoms as a "macro".
-- <space> (no count) replays it.
vim.keymap.set('n', '<Space>', function()
local count = vim.v.count
-- CmdAtom is deferred; schedule it so pending events land in the ring first.
vim.schedule(function()
count = math.min(count, #atom_ring)
if count == 0 then -- Replay the saved macro.
for _, step in ipairs(vim.g.atom_macro or {}) do
vim.api.nvim_feedkeys(vim.keycode(step.keys or step.lhs), step.keys and 'n' or 'm', false)
end
return
end
local parts = {}
for i = #atom_ring - count + 1, #atom_ring do
local a = atom_ring[i]
local keys = a.keys or ('%s%s'):format(a.count or '', a.lhs)
local field = a.keys and 'keys' or 'lhs'
parts[#parts + 1] = ('{%s=%q},'):format(field, vim.fn.keytrans(keys))
end
local cmd = ('lua vim.g.atom_macro = { %s }'):format(table.concat(parts, ' '))
-- Draft it on the cmdline; CTRL-F opens the cmdwin to edit it.
vim.api.nvim_feedkeys((':%s%s'):format(cmd, vim.keycode('<C-f>')), 'n', false)
end)
end)
By default, undo returns the cursor to its original position. If you don't
like that, you can use this snippet to get Vim's behavior instead:
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
-- Undo/redo: the buffer changed to an already-seen undo state.
local undid = ev.data.changed and (ev.data.undoseq or 0) <= (vim.b[ev.buf].maxseq or 0)
vim.b[ev.buf].maxseq = math.max(vim.b[ev.buf].maxseq or 0, ev.data.undoseq or 0)
if undid then
vim.cmd('normal! `[') -- Start of the changed text.
end
end,
})
Multiple repeat
Execute the Ex command [cmd] (default ":p") on the
lines within [range] where
{pattern} matches.
:[range]g[lobal]!/{pattern}/[cmd]
Execute the Ex command [cmd] (default ":p") on the
lines within [range] where
{pattern} does NOT match.
Example:
:g/^Obsolete/d _Using the underscore after
:d avoids clobbering registers or the clipboard.
This also makes it faster.
Instead of the '/' which surrounds the
{pattern}, you can use any other
single byte character, but not an alphabetic character, '\', '"', '|' or '!'.
This is useful if you want to include a '/' in the search pattern or
replacement string.
For the definition of a pattern, see pattern.
NOTE [cmd] may contain a range; see collapse and edit-paragraph-join for
examples.
The global commands work by first scanning through the [range] lines and
marking each line where a match occurs (for a multi-line pattern, only the
start of the match matters).
In a second scan the [cmd] is executed for each marked line, as if the cursor
was in that line. For ":v" and ":g!" the command is executed for each not
marked line. If a line is deleted its mark disappears.
The default for [range] is the whole buffer (1,$). Use "CTRL-C" to interrupt
the command. If an error message is given for a line, the command for that
line is aborted and the global command continues with the next marked or
unmarked line.
When the command is used recursively, it only works on one line. Giving a
range is then not allowed. This is useful to find all lines that match a
pattern and do not match another pattern:
:g/found/v/notfound/{cmd}
This first finds all lines containing "found", but only executes {cmd} when
there is no match for "notfound".
Any Ex command can be used, see ex-cmd-index. To execute a Normal mode
command, you can use the
:normal command::g/pat/normal {commands}
Make sure that {commands} ends with a whole command, otherwise Vim will wait
for you to type the rest of the command for each match. The screen will not
have been updated, so you don't know what you are doing. See :normal.
The undo/redo command will undo/redo the whole global command at once.
The previous context mark will only be set once (with "''" you go back to
where the cursor was before the global command).
The global command sets both the last used search pattern and the last used
substitute pattern (this is vi compatible). This makes it easy to globally
replace a string:
:g/pat/s//PAT/gThis replaces all occurrences of "pat" with "PAT". The same can be done with:
:%s/pat/PAT/gWhich is two characters shorter!
Complex repeat
Record typed characters into register
{0-9a-zA-Z"}
(uppercase to append). The 'q' command is disabled
while executing a register, and it doesn't work inside
a mapping and :normal.
Note: If the register being used for recording is also
used for y and p the result is most likely not
what is expected, because the put will paste the
recorded macro and the yank will overwrite the
recorded macro.
Note: The recording happens while you type, replaying
the register happens as if the keys come from a
mapping. This matters, for example, for undo, which
only syncs when commands were typed.
q Stops recording.
Implementation note: The 'q' that stops recording is
not stored in the register, unless it was the result
of a mapping
@{0-9a-z".=*+}@
Execute the contents of register {0-9a-z".=*+} [count]
times. Note that register '%' (name of the current
file) and '#' (name of the alternate file) cannot be
used.
The register is executed like a mapping, that means
that the difference between 'wildchar' and 'wildcharm'
applies, and undo might not be synced in the same way.
For "@=" you are prompted to enter an expression. The
result of the expression is then executed.
See also @:.
{Visual}@{0-9a-z".=*+}v_@-default{Visual}@@
In linewise Visual mode, execute the contents of the
register for each selected line.
See visual-repeat, default-mappings.
:[addr]@{0-9a-z".=*+}:@
Execute the contents of register {0-9a-z".=*+} as an
Ex command. First set cursor at line [addr] (default
is current line). When the last line in the register
does not have a <CR> it will be added automatically
when the 'e' flag is present in 'cpoptions'.
For ":@=" the last used expression is used. The
result of evaluating the expression is executed as an
Ex command.
Mappings are not recognized in these commands.
When the line-continuation character (\) is present
at the beginning of a line in a linewise register,
then it is combined with the previous line. This is
useful for yanking and executing parts of a Vim
script.
:[addr]@::@:
Repeat last command-line. First set cursor at line
[addr] (default is current line).
:[addr]@:@@
:[addr]@@
Repeat the previous :@{register}. First set cursor at
line [addr] (default is current line).
Multiple cursors
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).
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 sequences replay at each cursor: "viweex"
re-executes the selection so the extents are per-cursor (each cursor's own
word, block, …).
Undo is atomic: u reverts the aggregate edits from all cursors at once.
Registers are cursor-local. Each cursor reads/writes its own registers.
The yanks are joined (linewise) when the multicursor session ends.
Marks '< '> (gv), '[ '], '. '^ are cursor-local: "`." at each
cursor goes to its own last change. The jumplist and changelist are
global.
Folds are ignored during replay. Edits replay within folds ("dd" deletes the
line at cursor, not the entire folded contents). Motions step into folds
instead of skipping over them. The primary cursor keeps the usual
fold-behavior.
Cursors are per-buffer, and cascade only while their buffer is current.
Multicursors are presented as hl-MCursor highlights, unless your
(terminal) UI supports the Kitty multiple-cursors protocol.
CTRL-C interrupts the cascade.
Example: place a cursor on every matching pattern:
:g/pattern/normal! nQExample: place a cursor at each quickfix item, or a range:
:cdo normal! Q :2,4cdo normal! QExample: place a cursor at every named mark:
zq]`
Use [count] to force (un)placement instead of toggling:
"1Q" adds, "2Q" removes.
zq{motion}zq
Places a cursor at {motion}, repeated from the current
position until end of buffer (or limited to a Visual
selection: v_zq). Think of it like %:g as an operator.
A search motion ("zq*", "zqn", "zqgn") places a cursor
at every match, wrapping if 'wrapscan' is enabled.
Sets the Visual area (gv) per-cursor, so "gv"
selects the match at each cursor.
Examples:
"zqw": every word.
"zqgn": every match.
"zq*gvc" changes each match of the word under the cursor.
Not supported:
Text objects
Lua, <Cmd> or ":" mappings (except :map-<expr>).
blockwise-visual acts on each line of the block,
from the block's left column (or the end of a short
line, like v_b_A).
Examples:
"vipzqw": each word in the paragraph.
"vipzqj" (or "zq+"): each line.
"vipzq*": each match.
"<C-V>3jzql": each line of the blockwise column.
{Visual}Qv_Q<C-LeftMouse> Toggles a multicursor at the click position, without
moving the primary cursor (or changing windows). No-op
in Insert-mode.
q=q=
Toggles 'follow' mode: cursor-relative motions ("w",
"fx", "*", …) are replayed per-cursor. Cursors
arriving at the same position (e.g. "9j" on the last
line) are merged (deduplicated).
Not replayed:
Absolute motions (scrolls/jumps: "G", global marks,
CTRL-O).
Motions that fail/beep ("j" on the last line).
Use [count] to force the mode instead of toggling:
"1q=" on, "2q=" off.
CTRL-Lmcursor-clear
Cursors can also be cleared by clearing the namespace:
local mc_ns = vim.api.nvim_create_namespace('nvim.multicursor')
vim.api.nvim_buf_clear_namespace(0, mc_ns, 0, -1)
gQgQ
Restores the previous multicursors, and the primary
cursor position. No-op during a multicursor session.
g
During a multicursor session, inserts an ascending
number ("counter") at each cursor, so a column of
cursors becomes 1, 2, 3, …. Use [count] to choose the
initial number.
CTRL-Ag_CTRL-A]C]C
Jump to the [count]'th next cursor.
Hint: "]CQ" removes cursors one at a time.
[C[C
Jump to the [count]'th previous cursor.
Without a multicursor session, "]C" and "[C" jump to
the previous session's cursors (gQ), and work as an
exclusive motion: "d]C" deletes to the next one.
API
Plugins can place cursors by setting or deleting extmarks in the
"nvim.multicursor" namespace, and inspect user actions via the CmdAtom
event.
Adding an extmark (nvim_buf_set_extmark()) adds a cursor, unless one is
already there. Query them with nvim_buf_get_extmarks(). Deleting an extmark
deletes the cursor.
You can also use :marks to peek at cursors:
:marks nvim.multicursor
In rare cases a plugin may need to check v:cascading to change its
per-cursor behavior. Such special handling is generally discouraged;
multicursor is intended to work implicitly, without plugin modifications.
EXAMPLES
Example: a "]C" mapping which places a cursor before jumping to the next, so
you can "rotate" through the cursors.
local ns = vim.api.nvim_create_namespace('nvim.multicursor')
vim.keymap.set('n', ']C', function()
local row, col = vim.pos.cursor(0):to_extmark()
vim.api.nvim_buf_set_extmark(0, ns, row, col, {})
vim.cmd('normal! ]C')
end)
Example: a "follow-once" mapping: "q-{motion}" replays
{motion} at every
cursor, then disables 'follow' mode again.
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)
Example: place a cursor at every match, using the API:
local ns = vim.api.nvim_create_namespace('nvim.multicursor')
for _, m in ipairs(vim.fn.matchbufline('%', [[pattern]], 1, '$')) do
vim.api.nvim_buf_set_extmark(0, ns, m.lnum - 1, m.byteidx, {})
end
LIMITATIONS
During Insert-mode completion, live mirroring pauses if a preview could
shift or overlap the primary's completion (same-line edits before the
primary, or line insertions/deletions before it).
During Insert-mode, edits by a
<Cmd>/Lua/API are not replayed per-cursor;
the primary text is mirrored as-is (similar to insert-completion).
Cursor is removed if its Insert-entering operation fails. E.g. "ct;" on
a line without ";", "cgn" with no match.
Explicit clipboard registers (
"*yy, "+yy) are global, not per-cursor. On
multicursor exit, you can set the clipboard to the joined yank::let @+=@"
Implicit 'clipboard' ("unnamed"/"unnamedplus") updates the clipboard on
multicursor exit.
Same-line cursors may be lost/misplaced by an edit that shifts columns (e.g.
inserting text).
Reloading/unloading a buffer (:edit!, 'autoread') clears its cursors
(extmarks limitation).
zq at the start of a line guesses whether the start is a step: at the
start of the buffer it always is ("ggVzqw" on an indented line), elsewhere
"f"/"t" skip it ("0zqfa" on a line starting with "a").
A mapping or function that does not re-read its input will not cascade
correctly.