Git Gud at Vim 3: Registers and Macros

This is a write-up of a series I presented to a small group in 2018. To see all the articles, see the link below or find more vim related articles.
Macros let you record complex edits into a register to make tedious editing tasks easier.
The last two weeks we've talked through the basics of how Vim thinks and talks about the files and buffers, and how you can describe an edit to Vim. This week we are going to pull all that together with macros.
macro _| ˈmakrō |_ noun (plural macros) 1 (also macro instruction) _Computing_ a single instruction that expands automatically into a set of instructions to perform a particular task.
As the dictionary describes, a macro essentially lets you group multiple changes and motions together into a single instruction. You can write macros that strip M$soft Word HTML tags out of documents, save backup copies of files, fix line endings, or even solve mazes.
I used to use macros as a last-ditch effort for solving a complex problem that crops up over and over, but over the years I've realized that it is so quick to make them... I make little temporary ones all the time.
In order to be an effective user of macros, it helps to have an understanding of registers and marks. We will also discuss some other neat Vim features that play into macros.
Registers
In computing theory, a register is a small memory spot for storing things that
you are using currently. That's exactly how Vim treats its registers. There are
a number of named registers, and some special registers. You can see the states
of all currently set registers by running :registers
.
Vim lists ten kinds of registers! (Don't worry, we'll only discuss a few in detail.)
1. The unnamed register "" 2. 10 numbered registers "0 to "9 3. The small delete register "- 4. 26 named registers "a to "z or "A to "Z 5. three read-only registers ":, "., "% 6. alternate buffer register "# 7. the expression register "= 8. The selection and drop registers "*, "+ and "~ 9. The black hole register "_ 10. Last search pattern register "/
"[0-10]
Register 0 contains the text from the most recent yank (y
) command, unless
the command specified another register.
The registers under the numbers 1-9 are automatically filled when you delete
(d
or x
) characters unless you specify a different register. As you might
guess, the first thing you delete goes under 1, then when you delete something
new it moves to 2 and the newest goes to 1, etc. There is some complexity to
this, but it's nice to know that the things you delete are "saved" somewhere,
if only temporarily.
"[a-zA-Z]
I didn't do this very elegantly, but here are the basic steps:
- I moved the cursor to a
<p>
tag. "Myit
(appending to register m, yank inside tag)- In insert mode,
<c-r>m
to insert the contents of m. :registers
to show you all the registers including m.
The main registers you'll use for macros are under the letters [a-z]. If you put something in a register under its lowercase character, it'll replace that register's contents. If you use its uppercase character, it'll append to the register's current contents, very handy for grabbing content out of multiple locations (like <p> tags on a page!)
To specify a register for a yank or delete, start with "<registername>
followed by the rest of the command. (For example: "ax
will delete the
current character and put it in register a
, and "Ax
will delete the current
character and append it to register a
)
The Expression Register "=
This is an odd one, and I tend to leave it alone. When you access it, it prompts for an expression to put in... you can put in some simple math, a function, really anything you can run in vimscript. I have mostly used this one a simple calculator. There are more ideas on this stack overflow question.
Last Search Pattern Register "/
5j
to move to the line with the horrid variablefG
to move to that wordgd
to put that word in the search register- Start a global search/replace
%s
-
Use
<c-r>/
to insert the results of the search register into a command, complete with word barriers to replace with "SHORTCUTS". - Repeat the first 3 steps to show how SHORTCUTS has now replaced all over the file.
This is one of my favorite weird vim tips. There are a couple ways to "search"
for something... you can search using /
in normal mode, or you can hit gd
on a word and it'll put the current word in the search register as it attempts
to find a local declaration. I'll often do this intentionally to dump the
current word into the search register, to then use it in a %s/<from>/<to>/g
find and replace.
Protip: You can use registers in commands, in buffers... even in other registers.
Macros
Now that we have learned a little bit about Vim's registers, we are ready to discuss Vim's macros. We learned in week 2 that we control vim through a series of combined Operators and Motions, and all through that article we showed how you can write out those changes as strings. Well, that's all a macro is: A string of text stored in a register representing a series of recorded actions to be replayed.
Once again, not my smoothest work, but it's real life so here it is.
-
Move the cursor to the beginning and use
qq
to start recording a macro to register "q". -
Use
^
movement to start the macro, making sure I'm at the beginning of the line. -
/href
to search for the line with the <a> tag. -
"byit
to yank inside tag into register b (for "body!") -
?href
search backwards for that href again. -
f"
to move forward to the quotes. I'm headed to grab the URL. -
"ui"
to yank inside quotes into register u (for you guessed it, URL.) -
?<li>
to find the opening <li> element -
<shift-v>
to start a visual selection -
/<\/li>
to search forward for the closing <li> tag. d
to delete selection.-
Type in the new template, using
<c-r>b
and<c-r>u
paste in the body and the URL. -
v
to start selection,%
to jump to the matching "<" character, and<shift-j>
to join the lines to remove most of the troubling whitespace. j
to move down a line- repeat by running the macro with
@q
I use macros especially for small, tedious refactor tasks. I've spent a lot of
time working with HTML and CSS as a front-end developer. I often will come
across a bug in my front-end where I can't solve it just in CSS, I need to
refactor some crufty old HTML that has been copy-pasted in this file for years.
This is where macros shine. I need to preserve content, reorganize and use
some new CSS classes... Vim makes me faster at doing these small changes, but
why should I do them more than once? If I can use Vim to fix one of the broken
<div>
tags, I can record those changes and replay them on every example
in the file, or in the project.
To record a macro, starting in Normal mode, you press q<register>
, where
<register>
is the name of the register you wish to store this sequence in.
You then will see a message in the statusline letting you know you are
recording to that register, and then as you move and type your actions are
recorded as a string to that register. You can see that string by typing
:registers
.
To run the contents of a register as a macro, in Normal mode you use
@<0-9a-zA-Z>
, you can think of this as "run the macro at (name of
register)".
Pro Tip: If you are working on a macro and you do something you don't like... you can even paste out that register to a buffer (make sure to hit
<c-r><c-r>
to paste the register exactly, a single<c-r>
will evaluate the content of the registers referenced in the macro), edit it in Vim, and yank it back into that register to replay later as a macro. Because macros are just text, you can share, modify, even have one macro change the text of another register in order to modify the next macro. Meta!
Marks
Marks are a way of saving a location in a file. You can set a mark in Normal
mode by pressing m<0-9a-zA-Z>
. You recall a mark's line with '<0-9a-zA-Z>
and a mark's full location with `<0-9a-zA-Z>
Lowercase marks are
local to a file, but uppercase marks work between files. Before I learned about
Vim's jumps I'd use marks to jump between files that I'm looking at, like an
HTML and CSS file.
Marks are used for several things in Vim internally. We haven't discussed the
visual modes except in passing at the moment, but if you set the marks for "<"
and ">" you'll actually be doing the same thing as making a visual selection.
You can try this out by setting those marks and then pressing gv
for "go
visual" in Normal mode.
Now, I use marks inside macros as a way to set locations for the cursor while working on a fragment of code. I'll yank to registers and mark locations as I go, so that it makes it easier to work my way back through them on a second pass.
Jump Lists
Jump lists are like marks that are set automatically as you move through the
buffers in Vim. You can see them at any time using :jumps
. You can move back
to the previous location using <c-o>
(out) and move back using <c-i>
(in).
I use this constantly. If I use go to tag (<c-]>
), and I want to go back to
the file I was just editing, I just hit <c-o>
to move back out to the
previous file. Once it gets under your fingers it is wonderful.
MOAR MACROS
Hooks
Now that we have a lot of cool tools to choose from, let's talk about the strategy of using a macro to refactor. (There are many other uses for a macro... pretty much anything that you find yourself doing over and over in Vim is a good candidate for a macro.)
When facing some code that would benefit from a macro (3+ code structures with
deep similarities in structure or content that all have to be changed the same
way,) the first thing I do is to take a quick scan down the structures I need
to change looking for unique markers that I can use f
or t
to jump to. I
call this "looking for hooks." I'm looking for the commonalities that will make
this change repeatable.
More often than not, those unique markers are delimiters like quotes or brackets, but you have to look for "gotchas".
<li>
<a class="link" href="/help">
Get Help
</a>
</li>
<li>
<a class="link" href="/about-us">
About <em>Our</em> Team
</a>
</li>
One common mistake with macros is to not "reset" the state at the beginning and end of a macro. I will often use a
0
(placing the cursor at the first column in a file) or^
(placing the cursor at the beginning of a line) before I start working commands.
In this example, if I'm trying to robustly copy the content of the <a>
tag, I
would have problems if I searched for the <
character to find the next HTML
tag. It would work for the first <li>
, but the second has an HTML tag in its
content. Searching for </
won't work either... I'd probably wind up using
yit
(yank inside tag) to trust Vim's built in parser to pull the whole
content inside the tag and sidestep this problem entirely... and then probably
have to deal with the extra whitespace I just yanked.
Nested Macros
One strategy I've employed is to break up a large macro into smaller macros. This is a good strategy when you are writing a macro that transforms a whole file in multiple passes, or a macro that needs to work on multiple files. Because macros are just Vim commands, there's nothing that you couldn't do in a macro.
Protip: If you find yourself using a macro like a command that you use often, it's usually better to put it into your Vim config file as an actual command. One macro that migrated from my registers into my config is one that strips trailing spaces out of a file.
Modal Editing + Macros = Awesome
When I first stated using macros, they were a very specialized tool for when I was most frustrated. After nearly a decade of using Vim, I use them nearly any time I have a repetitive editing activity that's more demanding than the repeat operator and less intense than a script in some programming language that I should check into source control. They are a central part of why I can't seem to quit vim, and not for the main reason that others make that claim.
If you enjoyed what you've learned so far, shoot me a line or check out some of the great links below.
More Reading:
Changelog
-
2022-06-08 11:31:29 -0500Rename articles
-
2022-06-07 15:27:31 -0500Adding Admonitions
-
2020-06-18 14:26:02 -0500Move everything to CST
Don't know why I didn't do that before. It caused _no_ end of
problems. -
2020-06-14 15:51:50 -0500Update articles with series and series partials
After the refactor, needed to move the series metadata to the actual
name. -
2020-06-08 12:17:07 -0500Hack together a series page
I'm basically hacking this into the "tags" system... if you have a tag
named "series-<rest of tag>", it changes the tag page to a "series" page
that also uses the same partial as a series post. -
2020-06-08 10:48:51 -0500Create series partial
I tend to write series blog posts, and I have one coming up... so I have
refactored how I used to do series to test it out. -
2019-11-14 19:21:38 -0600Remove Tag: programming
`code` is the same tag and has more coverage.
-
2019-02-06 20:07:54 -0600Remove test code I made while playing around
I think I was demoing macros to a group of students. :P
-
2019-01-07 11:40:34 -0600Add missing "vim" tag
-
2018-11-06 22:42:25 -0600Final Vim Post: First Version
-
2018-11-04 21:49:24 -0600Fix tag
-
2018-10-29 14:57:30 -0500Add final example
-
2018-10-29 14:43:17 -0500Make the long mini-cast full screen
-
2018-10-29 14:39:19 -0500Finish filling out outline of blog post
-
2018-10-18 10:33:08 -0500WIP