VIM: rapidly exchange text interposed by varying delimiters

2020-12-18 @Technology

Another operation I consistently performed manually was the exchanging of words across a line. Now to merely exchange the current and the next consecutive word, for many years I’ve applied the following mapping to a search/replace operation:

Exchange two consecutive words

" Switch the current and next word
nmap <F3> :s/\v(<\k*%#\k*>)(\_.{-})(<\k+>)/\3\2\1/<CR>``<C-L>

Explanation (feel free to skip if uninterested.)

The above search/replace regex pattern actually acquaints you with some lesser used VIM regex features. The %# pattern, for instance, matches against the current cursor position. Hence <\k*%#\k*> ends up matching the current word, < and > being the word boundaries and \k* representing zero or more keyword characters to the left and right of the cursor position. (You could probably use \w as well.)

That makes the current word pattern. Next follows the second, between the words pattern, which would consist of one or more non-word characters.

And \_.{-} is a useful construct I didn’t grasp until recently. While . in a pattern represents any character, \_. does something extra: it also matches a new line. And since we’re replacing consecutive words on potentially two separate lines, this is precisely what we need.

Lastly, but crucially, is the {-} quantifier. If we simply used \_.* (zero or more of any character, the pattern would greedily consume everything to the line’s end. Hence we’d never match the following word. {-}, on the other hand, is the non-greedy variety, matching the minimally necessary to give preference to the pattern that follows, which, in this case, is <\k+>, a word of one or more characters.

And since we’ve surrounded each of the three patterns with parentheses and made them atoms, \3\2\1 on the replace side repositions the two words, leaving the middle separator intact.

More powerful exchange via delimiters

Now what if we’re to exchange something more complex, for instance:

text text source text1, replacement text2, text text

Or

text text source text1 (replacement text2) text text

The above involves not only multiple words but varying delimiters. In the latter case, the second word combination is even surrounded by two different delimiters, ( and ).

I could take about five-seven seconds to exchange the two parts via a handful of VIM keystrokes, transitioning one side to the other and vice-versa, some awkward movements possibly involved as well. Seven seconds isn’t much time, but manipulations like this occur all the time, and I wanted something nearly immediate.

After all, the generalized problem is simple:

Take everything from the current word to the first delimiter, and exchange that with everything after until the second delimiter.

Now we could easily incorporate an established set of delimiters into a search/replace pattern. But I wanted something more flexible. I wanted to indicate both delimiters as operand keystrokes onto a mapping. (I wasn’t about to define individual mappings for every delimiter combination I encounter.)

And only lately did it dawn upon me that VIM provides an <expr> mapping construct allowing you to programmatically build a mapping - that is, to rise one level of abstraction higher and programmatically construct an already programmatic sequence of expressions.

Such flexibility is the beauty of meta programming languages!

First I defined the following function that accepts as input delim, the first delineating character, and term, the terminating character (second delimiter, if you will):

function! SwitchFieldsUntilTerm (delim, term)
    exec ':silent! s/\v(\k*%#.{-})(\s*\' . a:delim . 
                \ '\s*)([^\' .  a:term . ']*)/\3\2\1/'
endfunction

The function executes a similar, although more intricately constructed search/replace expression which I will not break down, since it doesn’t present anything new over the word-replace expression we started with.

Now just remains to call the function via the expression-based mapping:

nmap <expr> ,s ':call SwitchFieldsUntilTerm("' . 
        \ nr2char(getchar()) . 
        \ '","' . nr2char(getchar()) . '")<CR>'

The key portions are the nr2char(getchar()) calls, which wait for a one-character input, converting that from a numeric code to a printable character.

Note that we build the mapping as an expression, concatenating the static parts (surrounded by quotes) with the dynamic calls to nr2char(getchar()).

Fantastic. Now whenever we encounter the following text,

text text source text1 (replacement text2) text text

Place the cursor over ‘source’ followed by the normal-mode stroke combination ,s(), which transforms it into the following:

text text replacement text2 (source text1) text text

Similarly, with cursor over ‘source’, ,s,, transforms

text text source text1, replacement text2, text text

into

text text replacement text2, source text1, text text

Likewise, if the terminating delimiter is not to be found, the replacement text is consumed to the end of the line.

Thus, with cursor over ‘source’, ,s|| transforms

leading text source text1 | replacement text2, trailing text

into

leading text replacement text2, trailing text | source text1

A thing of beauty.

Is there a drawback? Only that we can’t use this mapping/function with multiple, consecutive delineating (or non-word) characters such as in our simpler, consecutive word replacement mapping:

For instance, it will not work with source text1 #$%^ replacement text2, since our expression mapping can only accept one character for a delimiter.

But who cares, when this case is so rarely encountered and could easily be addressed by a few seconds of manual intervention? It’s the frequently recurring cases that we’re eager to automate!

Questions, comments? Connect.