Phil Booth

Existing by coincidence, programming deliberately

Interactive bulk editing with vim macros

Along with all the fun, creative stuff it enables you to do, programming sometimes requires you to carry out boring and repetitive editing operations. If those operations are uniformly applicable, it's straightforward to automate them using regular expressions and a tool like sed, or :%s/foo/bar/g in vim-speak. But sometimes a regex can't express the pattern you want to match against and on those occasions, vim macros can come to the rescue.

Case study

Let's look at a concrete example to see what I mean. JavaScript fat arrow functions have different semantics for this to regular functions, binding it to the value of this from their parent context. If you want to bulk edit a bunch of function expressions to fat arrows, you must also inspect the body of each function to see whether it references this. A simple regular expression match can't do that, so instead you need to step through the matches one-by-one and decide whether to apply the change after eyeballing each particular block of code.

Consider this code that uses mocha to run some unit tests:

describe('a unit test suite', function () {
  let result;

  before(function (done) {
    result = foo(done);
  });

  it('returned the correct result', function () {
    assert.equal(result, 'expected result');
  });

  it('flaky test', function () {
    this.retries(3);
    assert.didNotThrow(bar);
  });
});

Here, the final test calls this.retries, so is not suitable to be replaced by a fat arrow. Additionally, we must be careful to preserve the optional done argument in the before callback. In the real world of course, the example might be hundreds of lines long and spread over multiple source modules. Doing it manually is tedious busywork.

The first step to bulk editing this with macros is to search for a match pattern. For our example code, we can do that by typing the following in vim's command mode:

/function

...then hitting Enter.

That will land the cursor on the first function in our listing:

describe('a unit test suite', function () {

It doesn't reference this, so can be replaced with a fat arrow. We'll do that while recording a macro, so the same macro can be applied to subsequent functions too.

Macros are recorded by typing q in command mode, followed by a character to identify which register it should be stored in. As we're replacing functions here, let's use the letter f:

qf

We're now in macro-recording mode and every action we take will be added to the macro, including both movement and edits. The first action we need to perform is to delete the function keyword using dw.

Next we want to move the cursor to the closing parenthesis, but it's important to do so in a way that will work for all subsequent macro invocations. We can't simply use l because that wouldn't move past the optional done argument. Likewise w would behave inconsistently for the same reason. By moving straight to the closing parenthesis using % instead, it ensures we'll always skip past any intervening parameters.

With the cursor positioned over the closing parenthesis, we can append text with a and add the fat arrow. Then we can return to command mode with Escape and stop recording the macro with q. The macro is now stored in register f and the edited line of code looks like this:

describe('a unit test suite', () => {

To recap, the full sequence of keystrokes that we went through to record this macro were:

qfdw%a =>^[q

(where ^[ is the Escape key)

At this point we're all set, and can begin stepping through the remaining functions with n. That takes the cursor to the next function in our example listing, which is the before callback:

before(function (done) {

This is another valid case for replacement, so we can apply the macro that we just recorded by typing @ followed by the appropriate register letter, in our case f:

@f

In one fell swoop, our macro is applied and the line is changed to a fat arrow expression:

before((done) => {

Pressing n again moves the cursor on to the next function:

it('returned the correct result', function () {

Once more we have a valid candidate for replacement, so can invoke our macro a second time. We can do that with @f again, or we can use the "previous macro" shortcut:

@@

Either way, the edited line now looks like so:

it('returned the correct result', () => {

Pressing n a further time brings us to the function that we don't want to to update because it uses this:

it('flaky test', function () {
  this.retries(3);

We can skip past this one with n and continue working our way through the rest of the codebase in a similar fashion.

Persisting macros

Often the macro you create will be for a particular task and you can forget about it after that task is completed. But occasionally there will be macros that you want to re-use in the future and it makes sense to add those to your .vimrc file, to save the effort of recording them again every time you need to apply them. Doing that is easy.

Continuing with our previous example, let's assume the macro we want to save is stored in register f. We don't want to lose it by terminating the session, so we open the .vimrc file in the same session instead:

:e ~/.vimrc

Next, we add a line to the .vimrc file where we're going to save the macro definition:

let @f = ''

Now we want to print the contents of register f between the two quote characters. We can do that with the cursor over the opening quote by typing the following in command mode:

"fp

Or if the cursor is over the closing quote of course:

"fP

Here f is just the register name, so if our macro had been saved in a different register we'd replace f with another letter instead. Regardless, the line will now look like this:

let @f = 'dw%a =>^['

(except the ^[ will be a real Unicode Escape character, decimal 27 / hex 1B)

In a fresh editing session you can now check the macro has been saved by finding a function and typing @f in command mode. All being well, you should see it change to a fat arrow expression.

Conclusion

So, in summary: