Git Gud at Vim 3: Registers and Macros

vim icon

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]

showing the results of the `:registers`
In this mini-cast, I show how Vim remembers your deletions in the numbered registers, and has a separate register for your yank (copy). Notice how the delete includes the "^J" which signifies it copied the line-feed character as well.

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:

  1. I moved the cursor to a <p> tag.
  2. "Myit (appending to register m, yank inside tag)
  3. In insert mode, <c-r>m to insert the contents of m.
  4. :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 "/

  1. 5j to move to the line with the horrid variable
  2. fG to move to that word
  3. gd to put that word in the search register
  4. Start a global search/replace %s
  5. Use <c-r>/ to insert the results of the search register into a command, complete with word barriers to replace with "SHORTCUTS".
  6. 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.

  1. Move the cursor to the beginning and use qq to start recording a macro to register "q".
  2. Use ^ movement to start the macro, making sure I'm at the beginning of the line.
  3. /href to search for the line with the <a> tag.
  4. "byit to yank inside tag into register b (for "body!")
  5. ?href search backwards for that href again.
  6. f" to move forward to the quotes. I'm headed to grab the URL.
  7. "ui" to yank inside quotes into register u (for you guessed it, URL.)
  8. ?<li> to find the opening <li> element
  9. <shift-v> to start a visual selection
  10. /<\/li> to search forward for the closing <li> tag.
  11. d to delete selection.
  12. Type in the new template, using <c-r>b and <c-r>u paste in the body and the URL.
  13. v to start selection, % to jump to the matching "<" character, and <shift-j> to join the lines to remove most of the troubling whitespace.
  14. j to move down a line
  15. 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.

Here, I use the macro from the previous example and show you some neat things with it.

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.

More magic with registers... using them as a simple templating system. Registers can refer to other registers, so why do what I did before and type out the whole thing when you can a template that references other registers and swap the registers out using macros?

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 -0500
    Rename articles

  • 2022-06-07 15:27:31 -0500
    Adding Admonitions

  • 2020-06-18 14:26:02 -0500
    Move 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 -0500
    Update 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 -0500
    Hack 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 -0500
    Create 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 -0600
    Remove Tag: programming

    `code` is the same tag and has more coverage.

  • 2019-02-06 20:07:54 -0600
    Remove 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 -0600
    Add missing "vim" tag

  • 2018-11-06 22:42:25 -0600
    Final Vim Post: First Version

  • 2018-11-04 21:49:24 -0600
    Fix tag

  • 2018-10-29 14:57:30 -0500
    Add final example

  • 2018-10-29 14:43:17 -0500
    Make the long mini-cast full screen

  • 2018-10-29 14:39:19 -0500
    Finish filling out outline of blog post

  • 2018-10-18 10:33:08 -0500
    WIP