When trying to explain why Vim is so amazing, people often say things like “you don’t have to use the mouse” and “modal editing” and “commands are composable” and such. These are all pretty nice things that make coding faster and easier. They also just so happen to lead to one of the most powerful and criminally underpublicized features of Vim: macros.
What Are Macros?
Macros are a form of “programmatic editing”, or using code to change your text. Macros are to pasting what normal mode is to insert mode. Pasting inserts text into your code. Macros run text as if it were a sequence of commands. Otherwise, they’re identical, down to even using the same registers to store text/keystrokes. If you write a macro to register a, you can type ”ap to paste the raw text of your macro to the buffer. If you yank the text “ZZ” into register a, @a will save and exit your window. This isomorphism has two consequences:
- If you want to edit a macro, you can paste it, change the text, and copy it back into your register, which is pretty handy.
- Running a macro is exactly the same as typing the keystrokes manually, meaning macros have acces to everything in Vim. A macro can split windows, create new files, run commands, even change settings. The only restriction is that a macro can’t directly record another macro.
Macros generalize commands: commands automate simple tasks, and macros automate complex ones.
Imagine we have a rails project with the following HTML:
<li id="apple">Apple Soup</li>
<li id="cotton">Cotton Soup</li>
<li id="bees">Bee Soup</li>
As part of a refactor, you are asked to convert the text in the list items to I18n. Depending on the setup, one way to do this would be with the following macro:
^"ayi""bcit<%=t 'soup_options.<c-R>a'%><c-C><c-W>wo<c-R>a: <c-R>b<c-C><c-W>wj
Where <c-X> is ctrl+X.
Thinking In Macros
Let’s dissect the macro we just used:
^ Move to beginning of line | Prefix
"ayi" Copy in quotes (id) to reg a |
"bcit Change in tag (text) to reg b |
<%=t 'soup_options. Text |
<c-R>a Paste reg a |
'%> Text |
<c-C> Exit insert mode | Body
<c-W>w Switch to next window |
o Newline |
<c-R>a: Paste reg a |
<c-R>b Paste reg b |
<c-C> Exit |
<c-W>w Switch to next window |
j Down one line | Suffix
The first thing is that there’s a clear structure to the macro:
- Most macros will start with a prefix, a set of commands that ensures your macro will run properly. Our setup is just ^ so that we’re at the beginning of the line. Other setups might involve starting at a specific string or character, jumping to a mark, clearing a register, etc.
- We follow with a body, which accomplishes the editing task.
- We finish with a suffix, which sets us up to repeat the macro. The macro still works without the suffix, but we’d have to move to the next line manually. With it, we’re already positioned on the next line and can just repeat with @q. We could also make the macro recursive. While a macro can’t record macros, it can run macros, including itself. If we appended @q to our suffix, our macro would repeat until it ran out of lines.
The body ends up being the most complex part, and there are a couple of special things worth noting about it.
Use of Registers
Registers are useful in vim commands; in macros they are absolutely vital. Registers are effectively your ‘variables’ in the macro, letting you store information for future transformations. Here we end up using two, one to store the id and one to store the text.
Similarly, marks are extremely useful, especially uppercase ones. While we used two windows here, we could have also placed a mark mA in our html file and mB in our I18n file and switched using that.
Because of this, it’s usually a good idea to reserve a few registers for use as macros and a few registers for use by macros.
Since the lines are all slightly different, we can’t rely on count-based commands to always be correct. Fortunately, vim has powerful relative motions and selection commands we can use instead. yi” and cit use text objects as invariants. We’re interested in the id and the li value, and “the first text inside quotes” and “the text inside the tags” will always describe them. Note that there’s multiple possible invariants to choose here, and that different contexts will change which ones are valid or invalid. Let’s say we instead had the following:
<li id="apple">Apple Soup</li>
<li id='cotton'>Cotton Soup</li>
<li class="foo" id="bees">Bee Soup</li>
yi” will yank “apples”, but “cotton” is in single quotes. We could do yi’ for “cotton”, but that would fail for “apples”. One invariant here is f=2lyw, or “yank the word starting two characters after the first =”. That would break for the third line (copying “foo” instead of “bees”). We could fix this by replacing f= with f>F= (last = before the first >).
(Other invariants here could be /id= or f>B3w (ew). Use the simplest one that matches your context.)
You start recording a new macro by pressing q[a-z] and end it by pressing q. You can append to an existing macro with q[A-Z]. You can also create macros by using “[a-z]y to copy a string of text directly into a register. A common way to write macros is this:
- Record the first iteration of the macro.
- Run on your second case and see if it still works. Undo if necessary.
- If the second run failed, paste the macro to a buffer, manually add the appropriate keystrokes, and copy it back into the register. Repeat as necessary.
Usually the issue is an incorrect invariant or a relative motion, and it should be easy to fix.
When directly editing a macro, you can use ctrl-V in insert mode to insert a special character literally. You can use this to add ctrl commands, escape, etc.
When Not to Use Macros
- When metacoding is unnecessary. You don’t need a macro to indent a page, since you already have gg=G.
- When another, simpler tool is sufficient. Vim provides a number of other commands to fill more common use cases, such as ., :norm, and :bufdo.
- When you have a consistent use case. Macros are intended to be quick, one-off transformations. If you expect to need it long term, or need something more flexible, consider writing a function instead.
- When you should be using a programming language instead.
Medium needs an inline-code format.