Record, view, edit, and replay Vim macros — sanely.
Vim macros are powerful but opaque: a register full of raw keystrokes you can't
read, can't fix, and can't trust. macro-lab.nvim opens any macro in a lab
window where it's broken down into human-readable steps, editable as plain
notation, replayable with a dry-run preview, and savable under a name.
╭─────────────────────── Macro Lab @a ───────────────────────╮
│ │
│ @a 3 keys · 18 bytes │
│ │
│ NOTATION │
│ │ ciwHello<Esc>jA<Space>world<Esc> │
│ │
│ STEPS │
│ │ 1 INS ciwHello<Esc> Change inner word, typing… │
│ │ 2 j Move down one line │
│ │ 3 INS A<Space>world<Esc> Append at end of line: … │
│ │
╰──── e edit r replay p preview s step w save q quit ────╯
Special keys, literal text, mode badges (INS/CMD/VIS/…) and section
headers are all themed via overridable MacroLab* highlight groups, so the lab
matches your colourscheme.
- View a macro as readable steps — a mode-aware interpreter groups the raw
keystrokes into logical actions (
ciwfoo<Esc>→ Change inner word, typing "foo") instead of a wall of bytes. - Edit a macro — open the macro as
:map-style notation, fix it by hand, and:wto write it straight back into the register. Round-trips losslessly. - Replay with preview — dry-run the macro against a throwaway copy of the buffer and see a before/after diff before you commit, or step through it one action at a time.
- Save named macros — persist macros to disk with a name and description, and load them back into any register.
- Convert repeated edits to a macro — after you've done the same edit a few
times by hand,
:MacroLabFromHistorydetects the repeated keystroke pattern and lifts it into a reusable macro.
Using lazy.nvim:
{
"willothy/macro-lab.nvim",
config = function()
require("macro-lab").setup()
end,
}setup() is optional — the :MacroLab* commands work as soon as the plugin
loads. Calling setup() additionally arms keystroke capture so
:MacroLabFromHistory works.
Requires Neovim 0.8+ (for keytrans()).
The typical "messy macro" flow:
- Record a macro the normal way:
qa…q. :MacroLab a— open the lab and read what you actually recorded.- Press
pto preview it on the current buffer (no changes made). - Press
eto fix the broken step,:wto save it back. - Press
rto run it, orwto save it under a name for later.
| Command | Description |
|---|---|
:MacroLab [reg] |
Open the lab for a register (default q). |
:MacroLabEdit [reg] |
Edit the register's macro as notation. |
:MacroLabReplay [reg] [count] |
Replay the macro on the current buffer. |
:MacroLabPreview [reg] |
Dry-run preview as a diff. |
:MacroLabStep [reg] |
Step through the macro interactively. |
:MacroLabSave <name> [reg] |
Save the register's macro under a name. |
:MacroLabLoad <name> [reg] |
Load a saved macro into a register. |
:MacroLabList |
List saved macros. |
:MacroLabDelete <name> |
Delete a saved macro. |
:MacroLabRecord [reg] |
Toggle native recording, then open the lab. |
:MacroLabFromHistory [reg] |
Lift a repeated edit into a macro. |
| Key | Action |
|---|---|
e |
Edit the macro as notation |
r |
Replay on the current buffer |
p |
Preview (dry-run diff) |
s |
Step through interactively |
w |
Save as a named macro |
l |
Load a saved macro |
L |
List saved macros |
<C-r> |
Refresh |
q |
Quit |
Defaults shown:
require("macro-lab").setup({
default_register = "q", -- register opened when none is given
record_register = "q", -- register :MacroLabRecord uses by default
storage_path = vim.fn.stdpath("data") .. "/macro-lab/macros.json",
history_size = 256, -- keystrokes kept for repeated-edit detection
min_pattern_repeats = 2, -- repeats required to detect a repeated edit
window = {
width = 0.6, -- <=1 is a fraction of the editor, >1 is columns
height = 0.7,
border = "rounded",
title_pos = "center",
},
keymaps = { -- buffer-local keys inside the lab window
edit = "e", replay = "r", preview = "p", step = "s",
save = "w", load = "l", list = "L", quit = "q",
help = "?", refresh = "<C-r>",
},
})Every colour in the lab is an overridable highlight group linked to a standard group by default. Override any of them to taste:
| Group | Default link | Used for |
|---|---|---|
MacroLabRegister |
Title |
The @a register heading |
MacroLabMeta |
Comment |
Key/byte counts |
MacroLabSection |
Statement |
NOTATION / STEPS headers |
MacroLabGutter |
NonText |
The │ gutter |
MacroLabKeys |
Identifier |
Literal keys |
MacroLabSpecialKey |
Special |
<...> special keys |
MacroLabIndex |
LineNr |
Step numbers |
MacroLabDesc |
Normal |
Step descriptions |
MacroLabBadgeInsert |
Added |
INS badge |
MacroLabBadgeReplace |
Removed |
REP badge |
MacroLabBadgeVisual |
Changed |
VIS badge |
MacroLabBadgeCmdline / MacroLabBadgeSearch |
Special |
CMD / FND badges |
MacroLabDiffAdd / MacroLabDiffDelete |
DiffAdd / DiffDelete |
Preview diff |
vim.api.nvim_set_hl(0, "MacroLabSection", { fg = "#7aa2f7", bold = true })local ml = require("macro-lab")
ml.open("a") -- open the lab for register a
ml.steps("a") -- get readable steps as a table
ml.notation("a") -- get the macro as :map notation
ml.replay("a", 3) -- run it 3 times
ml.preview("a") -- dry-run preview
ml.save("name", "a", "description")
ml.load("name", "a")
ml.list()
ml.delete("name")
ml.from_history("a") -- lift a repeated edit into register aA macro is just the raw bytes of a register. macro-lab converts those bytes to
readable :map notation with Vim's own keytrans(), tokenizes the notation
with a hand-written scanner, and runs the tokens through a small mode-aware
command grouper that recognises the shape of normal-mode commands (operators,
motions, text objects, insert/replace, ex and search lines, visual selections).
Editing goes the other way: notation is turned back into raw bytes with
nvim_replace_termcodes(), which round-trips cleanly with keytrans().
Run the test suite (no external dependencies) from the repo root:
nvim --headless -l tests/run.luaMIT