Ephraim
Ephraim

Reputation: 797

How to include a minimum of n spaces between two strings in Vim via substitute?

I have a text file with two words of different lengths on each line. For better reading, I want to have the second word on each line to be preceded with enough space characters to line up with the second words on all of the following lines.

For instance, given the text:

bla foo
barbla barfoo
foblaaaa Bablofoo

I want to have changed to:

bla      foo
barbla   barfoo
foblaaaa Bablofoo

Is it somehow possible via regex (e.g., s/…/…/g) in Vim to format the file like that? Something like the following,

:s/^\(\w\+\)\s*\(.*\)$/\1\t\t\t\2/g

but with the amount of necessary whitespace adjusted dynamically?

Upvotes: 3

Views: 225

Answers (2)

ib.
ib.

Reputation: 29014

An easy way to align text in columns is to use the Tabular or Align plugin. As I have shown in response to the question “Inserting indentation for columns in Vim”, it is possible to solve the problem using only built-in Vim capacities with the help of the following commands.1,2

:let m=0|g/\ze\>/let m=max([m,searchpos(@/,'c')[1]])
:%s//\=repeat(' ',m-col('.'))

The purpose of the first command is to determine the width of the column to the left of the separator (which I assume to be the end of the first word, \>). The width is calculated as a maximum of the lengths of the text in the first column among all the lines. The :global command is used to enumerate the lines containing the separator (the other lines do not require aligning). The \ze atom located just after the beginning of the pattern sets the end of the match at the same position where it starts (see :help \ze). Changing the borders of the match does not affect the way :global command works, the pattern is written such a manner just to match the needs of the next substitution command: Since these two commands could share the same pattern, the pattern can be omitted in the second one.

The command that is run on the matched lines,

:let m=max([m,searchpos(@/,'c')[1]])

calls the searchpos() function to search for the same pattern used in the parent :global command, and to get the column position of the match. The pattern is referred to as @/ using the last search pattern register (see :help "/). This takes advantage of the fact that the :global command updates the / register as soon as it starts executing. The c flag passed as the second argument in the searchpos() call allows the match at the first character of a line (:global positions the cursor at the very beginning of the line to execute a command on), because there is possibly no text to the left of the separator. The searchpos() function returns a list, the first element of which is the line number of the matched position, and the second one is the column position. If the command is run on a line, the line matches the pattern of the containing :global command. As searchpos() is to look for the same pattern, there is definitely a match on that line. Therefore, only the column starting the match is in interest, it gets extracted from the returning list by the [1] subscript. This very position equals to the width of the text in the first column of the line, plus one. So, the m variable is set the maximum of its value and that column position.

The second command,

:%s//\=repeat(' ',m-col('.'))

pads the first occurrence of the separator on all of the lines that contain it, with the number of spaces that is missing to make the text before the separator to take m characters, minus one. This command is a global substitution replacing an empty interval just before the separator (see the comment about the :global command above) with the result of evaluation of the expression (see :help sub-replace-\=),

repeat(' ',m-col('.'))

The repeat() function repeats its first argument (as string) the number of times given in the second argument. Since on every substitution the cursor is moved to the start of the pattern match, m-col('.') equals exactly to the number of spaces needed to shift the separator to the right to align columns (col('.') returns the current column position of the cursor).


1 These pair of commands can be rewritten as a single one:

:let m=0|exe'g/\ze\>/let m=max([m,searchpos(@/,"c")[1]])'|%s//\=repeat(' ',m-col('.'))

2 The remaining text copies the detailed description given in the aforementioned answer of mine.

Upvotes: 1

sidyll
sidyll

Reputation: 59327

That's probably not a regex work, as you have to compute the highest width for the first word by first iterating trough the file to then start including spaces.

If you really want to avoid Tabular as @Prince Goulash suggested, here is an interesting and easy solution:

let n = system("awk '{ if (length($1) > L) { L = length($1) }}; END { print L }' ".expand("%:p"))
%s/\s\+/\=repeat(' ', n+1-system("awk '{ print length($1) }'", getline('.')))

In the first line, the variable n will receive the output of a little awk program. It basically finds the largest width for the first field, that's the first work. Note the expand("%:p") at the end: instead of passing simply your file's name we are expanding it to a full path so you can avoid confusions with the current directory.

The next line is the actual substitution. It substitutes the first set of white space found with an expression. The expression returns a string of spaces repeated a certain amount of times, which is n (the maximum width) minus the length of the current line first word (awk help again!) plus one buffer space (you may use 2 or whatever number you want).

That will align everything.


If you want to install Tabular, then open your buffer and run:

:Tab / 

Notice there is a space after the forward slash.

Now the one million dollar question: do you want to install Tabular?

Upvotes: 1

Related Questions