Nested Replacements

While updating the TextMate source to use ARC I needed to update the following code:

NSAutoreleasePool* pool = [NSAutoreleasePool new];
…
[pool drain];

To use the new autorelease blocks:

@autoreleasepool {
    …
}

Searching

We can find all the autorelease constructs using a regular expression like the following:

NSAutoreleasePool.*(?m)(.*?)\[pool \w+\];

I made the assumption here that our autorelease pool variable is always named pool. If that is not the case, one could use a back-reference.

As for the pattern: The (?m) syntax enables “multiline matching”, meaning that the “any” operator (.) matches newlines. This isn’t enabled until the second use of ., so the first .* will only match the first line (excluding its newline).

The code between declaring the pool and draining/releasing it is matched by .*? wrapped in parentheses to capture it. As we are in multiline mode, we make it a non-greedy match by using the non-greedy modifier (?), i.e. using .*? instead of just .*.

Had we not made it non-greedy, and a file contained more than one use of an autorelease pool, we would have gotten just one match from that file, which would encompass all of the pools.

Replacing

A naive replacement string would be:

@autoreleasepool {$1}

If however you enter this after having done the find, you’ll get a replacement preview and notice that this isn’t satisfying:

Autorelease Replacement Preview

The problem is that the capture ($1) should be indented. Fortunately TextMate offers you its powerful format string syntax in the replacement field, which means we can perform further replacements on the capture. In particular we wish to increase the indent by one tab, which can be done using:

${1/^/\t/g}

What we do is match “begin of line” using ^ and then replacing that with a single tab (\t), specifying g as option to make it repeat (global). Looking at the preview though this isn’t entirely right either:

Indented Autorelease Replacement Preview

The problem is that the first and last lines are indented. What we really want is to only add a tab to lines which are non-empty. This can be done using the following replacement:

${1/^\t*(?=\S)/$0\t/g}

Here we match begin of line followed by zero or more tab characters and then not followed by whitespace. \S matches non-whitespace characters, where newline counts as whitespace, and wrapping it in (?=…) makes it a “look-ahead assertion” meaning that it is not part of the actual match, we just ensure the match is followed by this.

The entire replacement string now becomes:

@autoreleasepool {${1/^\t*(?=\S)/$0\t/g}}

Which gives us the desired replacement:

Final Autorelease Replacement Preview

Addendum

The ability to process captures when doing search and replace is not limited to regular expression transformations, TextMate offers conditional insertions, case transformations, and even an asciify conversion.

You can find the full range of options in Help → TextMate Help → Format String Syntax.