Substitution in Vim with expressions

2024-09-13
2 min read

Substitution in vim is very efficient, especially with the help of expressions. This is especially useful for calculations. I discovered this usage when I wanted to insert a list of numbers to the beginning of each line. But before go to the example, let’s take a look at expressions in vim substitution.

The expressions in vim are marked with “=”. This tells vim to treat the contents after “=” as expressions rather than literal texts. For example, vim will treat \=1+1 as 2 rather than the text 1+1. Using \=, I can insert numbers in the beginning of each line with expressions.

s/^/\=line(".")/

line(".") stands for the line number of the current line. I use ^ to capture the beginning of the line and add line numbers before each line and the output is:

19text1
20text2
21text3

\= can combine with printf() to achieve nicer output. To have a number before each line is good, but it looks better with a trailing dot and space . , so that it looks like a numbered list. I can do this with the following line.

s/^/\=printf("%s. ", line("."))/

%s tells vim that there is a string, and the string is the following argument line("."), the line number of the current line. Also, there is a dot and a space . after the string. This gives the following output.

33. text1
34. text2
35. text3

Vim can also do simple calculations with \=. For example, sometimes I want to insert the relative line number rather than the absolute line number. To do that, I first enter the Visual mode with V, and select the lines I want to work with. Type the colon : in the commandline will give us :'<,'>. This shows that the operations will be done within the selected part. The line number of the first line under selection is line("'<"), and the number of the last line is line("'>"). Also, the line number of the current line is “line(”.")". Therefore, the relative line number of the current line is line(".")-line("'<")+1. I added 1 to the number because the first line should also be 1. The previous command is revised into:

s/^/\=printf("%s. ", line(".")-line("'<"))/

And the output is:

1. text1
2. text2
3. text3

A tricky point in calculations in the expressions is that it cannot handle divisions, because the division sign / is the same as the separator / used in vim substitution. I can use # to surround the patterns that vim needs to findand put the substituted contents after the second #. For instance, I can divide the relative line numbers by 2, so that the line numbers would be 0, 1, 1, 2, 2, 3, 3…

s#^#\=printf("%s. ", (line(".")-line("'<"))/2)
0. text1
1. text2
1. text3
2. text4
2. text5

Now I know eough about expressions in vim substitutions, and I can go back to my original task to add numbers to the beginning of each line, and most importantly, I do not want the empty lines to interfere the numbering. So, what I have is:

text1

text2

text3

And what I want is:

1. text1

2. text2

3. text3

Basic calculation can do that. I can divide the relative line numbers by 2 and add 1, and the command is:

s#^.#\=printf("%s. ", (line('.')-line("'<"))/2+1)

Also, to capture the non-empty lines, I use ^.. That is to say, only add numbers on lines which have a character after the beginning of the line.

However, the outputis like the following. Because with ^., I substituted the beginning of the line together with the first character!

1. ext1

2. ext2

3. ext3

I have to add the first character back by submatch(0). submatch() returns matches from the pattern. With number 0, it returns the whole match, and with 1 to 9, it returns the first to the 9th submatch. So the command is:

s#\(^.\)#\=printf("%s. %s", (line('.')-line("'<"))/2+1, submatch(0))

Ta-dah! Here is the result!

1. text1

2. text2

3. text3

Useful links;