This was harder than I thought it was going to be. Naïvely, I expected something like
to work, but it turns out this global applies one
:join at a time to each matching line, so it doesn't work. (P.S.
:join is the Ex command that joins two lines; its normal-mode equivalent is
Here's a test case:
If you run the command I tried, you get
Instead, we need to be smarter: we need to find the "blocks" of numbers and then
:join them all at once. I had hoped to be able to do this with
global /pattern for digit-lines/,/pattern for non-digit lines/-1 join
but I couldn't quite make a negative patterns work (and I'm now disappointed that vim doesn't have a "negation" operator that applies to a pattern to produce its exact negative—that, or I misunderstood
\@! &co., and negative matching is just hard).
So I came up with the following solution (which should be quite fast, but will take time proportional to the length of the file):
function! GroupNums() abort
const line_pat = '^\s*\d\+,$'
while search(line_pat, 'W')
let first_line = line('.')
while getline('.') =~# line_pat && line('.') != line('$')
" now current line matches and is the last line, or doesn't match (and the
" end of the block is one above)
let last_line = getline('.') =~# line_pat ? line('$') : line('.') - 1
execute first_line ',' last_line 'join'
Create this function (you can type it interactively if you want, but you can also paste it into
/tmp/code.vim and do
:source /tmp/code.vim if you want), then switch to the buffer to fix and run
With the test-case, I get
5, 6, 7,
8, 9, 10,
For the curious, the algorithm follows at a high level. Starting from the top of the file (
1), we find the start of a block (
search(line_pat, 'W')). We never wrap around, so we only process each block once. We record the start of the block (
line('$')). Then, we move down one line at a time (
+) scanning until we reach the end of the file or the end of the block (the inner while's condition:
getline('.') =~# line_pat && line('.') != line('$')). The loop invariant guarantees that one or both of the following are true:
- We are at the end of the file;
- The current line does not match the pattern.
If the current line matches the pattern, then we must be at the end of the file, so we can use that for the end of the block (
line('$')). Otherwise, we can use whatever the line before the current line is, because it matched and now the current line does not (
line('.')-1). (Notice that it does not matter if we are the end of the file and the current line does not match; the second case covers this adequately.)
Having reached the end of the block, we do a
:<start>,<end> join to group the lines together, and search for the next block. Of course, if we are already at the end, this search will fail because we do not wrap and because the pattern does not match already-joined lines, which the last line may be).