2018-09-27

VIM methods for inserting HTML tags

I have been converting some plain text to HTML and I often need to apply italics, bold, or underline tags to a range of text. Instead of typing them in manually, I would like to visually-select some text, then have Vim wrap the visual text in the tags. I am not looking for a plug-in. I want something simple that I can understand and modify as needed.

Method #1: Substitution

One idea is to use the :s (substitute) command. The %V operator lets us limit changes to the contents of the visual selection. Note that the second %V operator which matches the end of the selection actually matches the position just before the final character, so you have to adjust the pattern to match one additional character.

:s#\%V.*\%V.#<i>&</i>#

Here is the same command broken down into small pieces:

1:sBegin substitute command.
2#Pattern begins after this character.
3\%VMatch starting position of Visual selection.
4.*Match any number of characters.
5\%VMatch ending position of Visual selection.
6.Match one additional character.
7#End pattern. Replacement text begins after this character.
8<i>Insert begin-italics tag.
9&Insert all text matched by pattern.
10</i>Insert end-italics tag.
11#End replacement text.

This works well as long as the selection is limited to one Vim line. However, if the selection crosses a line boundary, the substitution will be performed on each individual line. This may not matter to you, at least for the basic HTML tags, but it could be a real problem for XML trees, and I find it ugly:

abc def
ghi jkl
mnop qrst

becomes

abc <i>def</i>
<i>ghi jkl</i>
<i>mnop</i> qrst

Method #2: Cut and paste

I remember seeing a macro that does something like this:

di<i><C-R>"</i>^[

If you are new to Vim macros, that looks like gibberish, so let's break it down, step by step:

StepModeCommandComments
1VisualdDelete the currently-selected text. The deleted text is stored in the special register " (double-quote).
2NormaliSwitch to Insert mode.
3Insert<i>Type in the literal tag, in this case an 'i' to start italics
4Insert<C-R>"Use Control-R to enter the contents of the " register, as if the user had typed that text.
5Insert</i>Type in the literal tag, in this case '/i' to end italics
6Insert^[Escape from Insert mode into Normal mode.

Note that the last item is not two characters '^' and '['; it is a single Escape character. This is easy to record in a macro by pressing Escape, but if you are editing macro text, it can be entered by Control-Q Escape or Control-Q Control-[.

This works well except for one case: when the visually-selected text falls at the end of the line. When you delete characters at the end of the line (but not the whole line) Vim moves the cursor position left one position. It prefers to have the cursor positioned "on" (before) a character instead of the empty space after the last character of the line. So when the macro above switches to Insert mode, it is inserting one character to the left of where we want the new text. Example:

vwxyz

becomes

v<i>xyz</i>w

Method #3a: Move and insert

A better idea is to move the cursor to the appropriate positions in the buffer and insert the tags. We can do that by using text marks. When you visually select text, Vim assigns marks to the beginning (<) and end (>) of that selection. If you leave Visual mode, those marks still exist until you select new text or make certain edits. We can also use explicit marks to save a character position and return to it later with the ` (back-tick) command.

At the end of the macro, I would like the cursor to be positioned at the end of the modified text. This is tricky because here is another case where having the visual selection cross a line break makes a difference. If the text is all on one line and you insert text at the beginning, the mark at the end of the selection does not move with its original text. If the text crosses a line ending and you insert text at the beginning, the text on the next line does NOT move, so the trailing mark is still positioned at the end of the text. Here is a faulty macro that demonstrates the problem:

vmy`<i<i>^[`y3la</i>^[
StepModeCommandComments
1VisualvLeave visual mode. The cursor will be positioned on the last character of the selection.
2NormalmySet mark 'y' to the cursor position.
3Normal`<Go to mark < which is the beginning of the last visual selection.
4NormaliSwitch to Insert mode.
5Insert<i>Type in the literal tag, in this case an 'i' to start italics
6Insert^[Escape from Insert mode into Normal mode.
7Normal`yGo to mark 'y' which used to be the end of the last visual selection. Note that the mark did not move when we inserted the tag above!
8Normal3lMove three characters to the right (the length of the <i> tag) to return to the original ending character of the visual selection.
9NormalaSwitch to Insert mode, this time appending text after the cursor position.
10Insert</i>Type in the literal tag, in this case '/i' to end italics
11Insert^[Escape from Insert mode into Normal mode.
abc def ghi jkl

becomes

abc <i>def</i> ghi jkl

but

abc def
ghi jkl

becomes

abc <i>def
ghi jk</i>l

Another problem with the macro above is that it does not handle cases where the visual area was selected 'backwards', for example, by placing the cursor on the last character of a word, entering visual mode, and moving back to the beginning of the word. In that case, when the macro leaves Visual mode, the cursor would be sitting on the first character of the word, which is not where the final tag should be inserted!

Method #3b: Move and insert (improved)

The macro below is my best solution so far. The explicit move in step 2 fixes the backward-selection problem. The "move to end" solution has to handle two cases. If the original selection was on one line, inserting the initial tag will cause the text to advance beyond `> (the end-of-visual-selection mark), so you could move to the end by using `> followed by f> to advance to the new end-of-tag character. If the original selection was on multiple lines, however, inserting the initial tag will not move the text at the `> mark, and f> will not work when you start on the character you want to find. In that case, we need a new mark anywhere to the left of the new '>' character at the end of the final tag, and it turns out that method works for both cases. The highlighted steps below implement that technique.

v`>a</i>hmz^[`<i<i>^[`zf>
StepModeCommandComments
1VisualvLeave visual mode.
2Normal`>Go to mark > which is the end of the last visual selection.
3NormalaSwitch to Insert mode and append text after the cursor position.
4Insert</i>Type in the literal tag, in this case '/i' to end italics
5Insert^[Escape from Insert mode into Normal mode.
6NormalhMove one character to the left.
7NormalmzSet mark 'z' to the cursor position.
8Normal`<Go to mark < which is the beginning of the last visual selection.
9NormaliSwitch to Insert mode.
10Insert<i>Type in the literal tag, in this case an 'i' to start italics
11Insert^[Escape from Insert mode into Normal mode.
12Normal`zReturn to mark 'z'.
13Normalf>Advance cursor to trailing '>' of ending tag </i>

You can save this in your vimrc configuration file. To place it in register 'i', use this line:

let @i='v`>a</i>^[`<hmz`<i<i>^[`zf>'