Categories
vim

How to merge two multi-line blocks of text in Vim?

327

I’d like to merge two blocks of lines in Vim, i.e., take lines k through l and append them to lines m through n. If you prefer a pseudocode explanation: [line[k+i] + line[m+i] for i in range(min(l-k, n-m)+1)].

For example,

abc
def
...

123
45
...

should become

abc123
def45

Is there a nice way to do this without copying and pasting manually line by line?

3

  • So…you want to join alternating lines? That is, you want to join line x with join x+2?

    – larsks

    May 25, 2012 at 19:36


  • 1

    No, I have two separate blocks. Pseudocode-ish: [a[i] + b[i] for i in min(len(a), len(b))]

    May 25, 2012 at 19:37

  • 2

    See a similar question (and answer!) here

    – NWS

    May 26, 2012 at 21:17

885

+100

You can certainly do all this with a single copy/paste (using block-mode selection), but I’m guessing that’s not what you want.

If you want to do this with just Ex commands

:5,8del | let l=split(@") | 1,4s/$/\=remove(l,0)/

will transform

work it 
make it 
do it 
makes us 
harder
better
faster
stronger
~

into

work it harder
make it better
do it faster
makes us stronger
~

UPDATE: An answer with this many upvotes deserves a more thorough explanation.

In Vim, you can use the pipe character (|) to chain multiple Ex commands, so the above is equivalent to

:5,8del
:let l=split(@")
:1,4s/$/\=remove(l,0)/

Many Ex commands accept a range of lines as a prefix argument – in the above case the 5,8 before the del and the 1,4 before the s/// specify which lines the commands operate on.

del deletes the given lines. It can take a register argument, but when one is not given, it dumps the lines to the unnamed register, @", just like deleting in normal mode does. let l=split(@") then splits the deleted lines into a list, using the default delimiter: whitespace. To work properly on input that had whitespace in the deleted lines, like:

more than 
hour 
our 
never 
ever
after
work is
over
~

we’d need to specify a different delimiter, to prevent “work is” from being split into two list elements: let l=split(@","\n").

Finally, in the substitution s/$/\=remove(l,0)/, we replace the end of each line ($) with the value of the expression remove(l,0). remove(l,0) alters the list l, deleting and returning its first element. This lets us replace the deleted lines in the order in which we read them. We could instead replace the deleted lines in reverse order by using remove(l,-1).

11

  • 1

    hmm… I only have to press enter once. And it won’t insert a space between the two halves. If there’s some trailing space on the lines (like in the “work it ” example), that will still be there. You can get rid of any trailing space using s/\s*$/ instead of s/$/.

    – rampion

    May 25, 2012 at 20:04

  • 1

    Thanks for the explanation. :sil5,8del | let l=split(@") | sil1,4s/$/\=remove(l,0)/ | call histdel("/", -1) | nohls seems to be even better since it cleans up the search history after running. And it doesn’t show the “x more/less lines” message requiring me to press enter.

    May 26, 2012 at 8:15


  • 12

    If you want full vim reference for that answer: :help range, :help :d, :help :let, :help split(), :help :s, :help :s\=, :help remove().

    – Benoit

    May 26, 2012 at 16:41

  • 17

    Making sure people like you want to post answers like this is why I became a moderator. Good show 🙂

    – Tim Post

    May 26, 2012 at 17:17

  • 1

    There is a problem if there is not a white space after the first 4 sentences.

    – Reman

    Jun 7, 2013 at 9:11

59

+150

An elegant and concise Ex command solving the issue can be obtained by
combining the :global, :move, and :join commands. Assuming that
the first block of lines starts on the first line of the buffer, and
that the cursor is located on the line immediately preceding the first
line of the second block, the command is as follows.

:1,g/^/''+m.|-j!

For detailed explanation of this technique, see my answer to
an essentially the same question “How to achieve the “paste -d ‘␣’”
behavior out of the box in Vim?
”.

6

  • E16: Invalid Range – but it works anyway. When removing the 1, it works without the error.

    May 26, 2012 at 7:56

  • And too bad you cannot accept more than one answer – otherwise yours would have a green mark, too!

    May 26, 2012 at 8:20

  • 3

    Very nice! I didn’t know about :move and :join!, nor what '' meant as a range argument (:help '') and what + and - meant as range modifiers (:help range). Thanks!

    – rampion

    May 26, 2012 at 14:10

  • @ThiefMaster: The command is designed to be used when the two range of lines to join are adjacent to each other, so that the cursor is initially located at the last line of the first block that immediately precedes the first line of the second block. (See the linked answer and question.) The command works as intended even if the number of lines in the blocks differs, although it gives an error message in that case. One can suppress error messages by prepending sil! to the command.

    – ib.

    May 28, 2012 at 11:00


  • 2

    @ib.: I think it would be a good idea to put the detailed explanation into this answer, too.

    May 29, 2012 at 18:16

45

To join blocks of line, you have to do the following steps:

  1. Go to the third line: jj
  2. Enter visual block mode: CTRL-v
  3. Anchor the cursor to the end of the line (important for lines of differing length): $
  4. Go to the end: CTRL-END
  5. Cut the block: x
  6. Go to the end of the first line: kk$
  7. Paste the block here: p

The movement is not the best one (I’m not an expert), but it works like you wanted. Hope there will be a shorter version of it.

Here are the prerequisits so this technique works well:

  • All lines of the starting block (in the example in the question abc and def) have the same length XOR
  • the first line of the starting block is the longest, and you don’t care about the additional spaces in between) XOR
  • The first line of the starting block is not the longest, and you additional spaces to the end.

5

  • Woah, that’s interesting! I never would have thought that it would work that way.

    – voithos

    May 25, 2012 at 19:55

  • 13

    This works as wanted only because abc and def are the same length. Block selection will keep the indents of the deleted text, so if the cursor is on a short line when putting the text, the lines get inserted between letters on the longer ones – and spaces appended to the shorter ones if the cursor was on a longer one.

    – Izkata

    May 26, 2012 at 23:02


  • Indeed… Is there a way to do it properly using block copy&paste? This is the easiest way to do it after all (especially if you cannot lookup the more complicated ways here for some reason).

    May 28, 2012 at 9:19

  • 2

    Like @Izkata says you will run into the problem of text getting inserted between the longer lines. To workaround that, just add more spaces at the end of the first line to make it the longest and then paste your block of text. Once that’s done, compressing multiple spaces to one is as simple as :%s/ \+/ /g

    May 30, 2012 at 5:41

  • 1

    set ve=all should help, see vimdoc.sourceforge.net/htmldoc/options.html#’virtualedit

    – Ben

    Aug 13, 2014 at 12:08