The Woodshed

Behind here, no one can hear you scream

Do You Believe In Nomagic?

Command Wrapping in Vim: A Case Study

A writeup on something I finally got around to doing: a custom wrapper for Vim’s :substitute command. Like most editor scripting, it was probably more work than it was worth, but if you use Vim you know that you use :s all the time. So if you have even a minor quibble with it, you get to multiply that quibble times the hundreds or thousands of times you’re going to use that command.

I had 2.5 quibbles with it, to be exact. Let’s get the half-quibble (quib?) out of the way. The default range that :s operates on is the current cursor line, though I tend to use it on the current line quite a bit less than on the visual selection or the whole file. And even the “convenient” way to operate on the whole file makes you type a %, which is clearly a B-teamer in the QWERTY world.

The more significant of my complaints involves its interpretation of characters in the ‘replacement’ pattern. Basically, the default behavior makes just enough sense that you think you understand it, until all of a sudden it bites you. (Specifically, you might think that every character therein is treated literally unless it’s preceded by a backslash [or is a backslash], but you’d only be mostly right.)

The other whole-number quibble is caused by the same underlying “feature” as the previous one: magic. This is Vim’s odd term for which characters in an expression are treated as metacharacters, instead of taken literally. There are 4 levels of “magic”: magic, no magic, very magic, and very no magic (and no, I’m not making that last one up). The option that controls this is global, and thus you are threatened to never change it or you’ll basically never be able to use anyone else’s code ever. The default value is magic, which works more or less okay for quick searches, but tends to require an ugly amount of escaping when you’re using :s or :g (this being my last bone to pick).

You’re allowed to override the magic setting of a particular pattern without changing the global value. This is done by prefixing the pattern with either \m, \M, \v, or \V in accordance with the levels mentioned above. Handy, but not pretty, and ain’t nobody got time for typing \v all over the place. Also, amazingly, you can’t affect the “magicness” of :s’s replacement pattern with these, and yet this pattern depends on the magic setting (which, remember, you aren’t allowed to change).

So now the stage is set for my wrapper around :substitute, which consists of a custom function and a couple of command aliases (cabbrevs).

Most of the coding time, however, was spent on getting a cabbrev to work how a rational person would want it to. Let’s say you pick ss for your abbreviation, like I did. Well, you only want certain sses that you type in a command line (or search, because cabbrevs apply there too for some reason) to be expanded to your command. (This is also why it’s never feasible to use cmap, which would overapply mappings ruthlessly).

So I wrote a couple auxiliary functions. One defines your cabbrev in such a way that, whenever it would be expanded, it runs a “hook” to control what’s actually outputted. The other is that hook—it looks at the current contents of the command you’d typed thus far to see if the abbreviation’s expansion should be outputted.

Anyhoo, here’s the code. You can just put it somewhere in your .vimrc. By default, you’ll get two new “commands”—:ss and :ssl. The first essentially makes your pattern very magic and your substitution no magic. The second makes the pattern very no magic and the substitution no magic. They both assume a range of the whole file unless you specify your own range when you use it (just like :g). So:

" Replace all foofoo in file with foo&bar
:ss/(foo)\1/\1&bar/g
" Replace the first (*_*) on the current line with (^v^)
:.ssl/(*_*)/(^v^)/

I’m still doing a bit of testing, but so far things seem to work!