One of the most powerful feature we have is the macro. We can record anything with macros and replay them, we can even say “replay it N times”. It’s a big beast, we can even do nested macros, but be careful, that can hurt your brain.

We have two parts:

  • Recording
  • Replaying

Recording

With q, we can start and stop recoding our macro.

q{0-9a-zA-Z"}  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

A quick and easily digestible summary: With q{letter} we can record and store our macro in the provided register. It’s important to remember, it stores the content in a register. Later we will talk about registers, and they are using the same backend, actually recording is only storing the content in a register and replay executes the content of a register, even if it’s a “macro” or not.

Replaying

@{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 |@:|.

It’s pretty self-explanatory, stop recording and save the result in the previously defined register.

Examples

It’s a bit hard to produce a useful and easy example for that. Let’s say we have a Go file with a lot of function and we want to break long function definitions into multiple lines, but we don’t want or can’t use the Options pattern.

func FunctionOne(ctx context.Context, name string, veryLongVariableName int, fancyExtra string, wantToDoIt bool) (bool, error) {
  // ... secret code lies here.
}

func ShortFunction(ctx context.Context, name string) string {
  // ... secret code lies here.
}

func FunctionTwo(ctx context.Context, definitelyNotName string, definitelyNotVeryLongVariableName int, definitelyNotFancyExtra string, definitelyDonNotWantToDoIt bool) (bool, error) {
  // ... secret code lies here.
}

Now imagine we have a file with 30 functions like these. Of course we can use a lot of tools, formatters, whatever we can, but we can do it with macros.

A good practice to start a macro always with 0 (beginning of the line) or ^ (first non-space character in the line).

There are a lot of options, but we will walk through a way that can demonstrate how macros are working (without nested macros).

First stand on the first function (anywhere) and start a macro, save it in register l, to do this we can use ql, now at the bottom of the editor a small test tells us we are in recording mode recording @l, if you see that line, it means you are in recording. It can happen accidentally for example if you want to quit with :q, but you did not press : for some reasons.

So we are in macro recording mode. First we move our cursor to the beginning of the line with 0, because it’s a macro and we don’t want to mess up with wrong starting position. It’s part of the macro, so when we execute a macro, it will always do that.

Let’s break the parentheses first.

  1. Move the cursor to ( with f and go to insert mode with a (i before the cursor, a after the cursor) and hit a single newline (I’ll mark it with <cr>) and exit insert mode (<esc>).
  2. Move to the closing pair with f), go to insert mode with i, add a , and a newline, and leave insert mode.
0f(a<cr><esc>f)i,<cr><esc>

Don’t worry, if you are doing it, not just reading, you will see all the changes real time on that line, so you don’t have to remember the whole sequence to do that.

No we have, if we leave the macro as it is, the result will be:

func FunctionOne(
  ctx context.Context, name string, veryLongVariableName int, fancyExtra string, wantToDoIt bool,
) (bool, error) {

Not we have to add a line break after all ,. To do that we have to move one line up first because right now we are on the 3rd line. Go to command mode (:) and replace all , with a ,<cr><tab> (you can ignore the tab if your vim does an autoformat on save, or you call an autoformat when you are done).

k:s/, /,\r\t/g<cr>

At this point we can stop recording with q.

Let’s see what’s in the register l now:

:registers l
Type Name Content
  c  "l   0f(a^M^[f)i,^M^[k:s/, /,\r\t/g^M

Wonderful, we can even save this in out vimrc.

Fine, we are done with one function, but we have another 29. Wait, not all of the functions are long, we want to break only long functions. Does not matter, we have a nice macro, we can use it to on ranges, or even pattern matching.

We want to reformat function signatures where the line is longer than 80 characters and we can use ^func .\{75\} as a pattern to do that.

:g/^func .\{75\}/norm! @l

Now we can see our nicely formatted code:

func FunctionOne(
  ctx context.Context,
  name string,
  veryLongVariableName int,
  fancyExtra string,
  wantToDoIt bool,
) (bool, error) {
  // ... secret code lies here.
}

func ShortFunction(ctx context.Context, name string) string {
  // ... secret code lies here.
}

func FunctionTwo(
  ctx context.Context,
  definitelyNotName string,
  definitelyNotVeryLongVariableName int,
  definitelyNotFancyExtra string,
  definitelyDonNotWantToDoIt bool,
) (bool, error) {
  // ... secret code lies here.
}

That’s it, we are done. I know it’s not as easy to digest as c, but mostly because it’s a beast and can do a lot. If someone can learn to use vim without mouse and use proper actions (like not pressing backspace 30 times to delete something), macro is just a wrapper with q[register]...q and @[register] to automate things.

Extra

We can execute macros while we are recoding macros, but make sure you are not executing the content of the same register you are using to store your new recording. For example we can split up the macro into two separate macros:

  • k: 0f(a<cr><esc>f)i,<cr><esc>
  • l: k@k:s/, /,\r\t/g<cr>

That way we have a macro in register k that breaks only the parentheses, and we have a macro in l that moves one line up, executes the macro in register k, and breaks the line at all , characters. In this case it does not seem useful, but it is possible, that you want to record a macro that does something, and calls a “submacro” N times like 20@l. For example formatting HTML from raw text and we want to open a <div> with class="block", put the next 4 paragraphs in <p>, and close the <div>. After that we can call the “outer” macro 30 times and each call will create a new <div> with a bunch of <p> blocks in it.

A few macros from my vimrc

-- mark todo item as done
vim.fn.setreg("x", "0t]rx0")
-- mark todo item as not done
vim.fn.setreg("c", "0t]r 0")

-- break long function names
vim.fn.setreg("l", "0f(a\n\27f)i,\n\27k:s/, /,\\r\\t/g\n")

-- break go error chain
vim.fn.setreg("m", "f:li\n  \27")