20

Is there a dedicated way to merge two blocks of text by interleaving lines, like passing from this:

a1
a2
a3
a4
  b1
  b2
  b3
  b4

to that:

a1
  b1
a2
  b2
a3
  b3
a4
  b4

in a few commands?

EDIT: I really like Sato Katsura's solution, here is how I have implemented it:

function! Interleave()
    " retrieve last selected area position and size
    let start = line(".")
    execute "normal! gvo\<esc>"
    let end = line(".")
    let [start, end] = sort([start, end], "n")
    let size = (end - start + 1) / 2
    " and interleave!
    for i in range(size - 1)
        execute (start + size + i). 'm' .(start + 2 * i)
    endfor
endfunction

" Select your two contiguous, same-sized blocks, and use it to Interleave ;)
vnoremap <pickYourMap> <esc>:call Interleave()<CR>
6
  • Now I'm curious - what's your use case? Are you block-renaming subtitles for a TV season?
    – VanLaser
    Aug 30, 2015 at 22:22
  • 1
    @lago-lito - thanks for the answer! Yes, Vim is quite versatile :) Your expression "eye-parsing" made me also think at scroll-binding two Vim windows.
    – VanLaser
    Aug 31, 2015 at 11:10
  • 1
    I'm having trouble using this, how are you selecting the two consecutive blocks? Do they need to be adjacent?
    – cbcoutinho
    Mar 16, 2018 at 1:08
  • 1
    @cbcoutinho Yes they have :) I'm not sure you could select them both otherwise. In the example I've show, I put my cursor on (say) b1, then I hit vip to select the whole chunk, then ,it which is the <map-I've-Picked>. Is it not working on your side?
    – iago-lito
    Mar 16, 2018 at 9:49
  • 1
    @iago-lito I think my issue was that I didn't map anything and was trying to invoke the function manually by selecting the block and then in visual mode executing :'<,'>call Interleave(). This caused a bunch of Start past end and E714: List required errors. Taking a closer look at the mapping, I see now that I should have hit <esc> once before invoking the function, which works as expected
    – cbcoutinho
    Mar 16, 2018 at 10:41

5 Answers 5

9

There is no dedicated way to do that (as far as I know), but yeah, it can be done with a few commands:

function! Interleave(start, end, where)
    if a:start < a:where
        for i in range(0, a:end - a:start)
            execute a:start . 'm' . (a:where + i)
        endfor
    else
        for i in range(a:end - a:start, 0, -1)
            execute a:end . 'm' . (a:where + i)
        endfor
    endif
endfunction

You can run it with :call Interleave(5, 8, 1). The first parameter is the first line to move, the second one the last line, and the third one where to move them. You probably want to turn on line numbers to see what you're doing (:set number).

This assumes the blocks don't overlap. See :help :move and :help range() to understand how the function works.

There are probably better ways to pick up the two blocks. There is a plugin floating around that is supposed to let you swap two blocks. I can't remember the name of the plugin, but the author (perhaps the famous Dr. Chip?) has put more thought into finding an interface than I did. :)

2
  • Sweet! I only need two arguments since the two blocks are contiguous and have the same size : start and size. With a homebrew function that retrieves those values from a selection, it'll just be perfect. I'm working on it. :)
    – iago-lito
    Aug 29, 2015 at 18:10
  • Interesting crosslink? ;)
    – iago-lito
    Feb 24, 2016 at 9:02
16

Here is another alternative:

:g/^a/+4t .
:+,+5d 

First copy the lines which are 4 lines below to the after the current line (:h :t) then delete the consecutive b lines (:h :d)

Even better is this command:

 :g/^a//^\s*b/m .

Which means, for each line starting with a find the next line starting with 'b' and move it to below the current line.

8
  • 1
    I got "E16: Invalid range" on the second command. I tried .+,$d instead, and that worked (as did .+,.+4d). Aug 29, 2015 at 21:50
  • Not sure why this happens Aug 30, 2015 at 7:15
  • 1
    no it doesn't. Read :h :range, you can always use direct numbering instead of a regex search Aug 30, 2015 at 18:37
  • 3
    @iago-lito The second trick always works, but you need to change /^\s*b to another :range. e.g. : select 1st block, execute '<,'>g/^/'>+1m.
    – dedowsdi
    May 13, 2019 at 3:35
  • 1
    @iago-lito It's essentially the same as Christian's answer. Nothing is hardcoded if you visually select the 1st block, '>+1 marks beginning of 2nd block.
    – dedowsdi
    May 13, 2019 at 7:45
4

If you want to have a little bit of fun with macros and marks you could try something like this:

  • First put a mark (here a) on the line containing a1 with ma

  • Go to the line containing b1 and mark it with mb

  • Start recording a macro in the register you want (here the register q) with qq

  • Insert the folowing in your macro: ddmb'apjma'b

  • Stop recording the macro with q

  • Play it as many times as needed with X@q where X is the number of time to play it.

To detail the macro:

dd mb 'a p j ma 'b
 |  |  | | |    |
 |  |  | | |    go back to line marked `b`
 |  |  | | |
 |  |  | | move of one line and replace the mark `a`
 |  |  | insert the deleted line under the line marked `a`
 |  |  go to line marked `a`
 |  mark the future line to move with `b`
 delete the line to move

Edit As lago-lito mentionned it in the comments this method will overwrite the marks and the buffers.

  • For the marks I don't think that's a real problem: I rarely use all 26 marks in a buffer and I think one will most of the time find 2 free marks.

  • For the buffer it is possible to save it in a temporary variable: Before recording the macro use :let saveReg=getreg('"') to save the register and once the action is done use :call setreg('"', saveReg) to get the register back to its previous state.

Anyway I must admit that this solution is just a quick workaround and isn't optimal: In my opinion Christan's answer is the best one and should be accepted because it doesn't mess with buffers and marks, doesn't force the user to create a function and shows the power of the global command.

2
  • Interesting. Unfortunately, this overwrites the content of marks and registers, which I may be using ;)
    – iago-lito
    Aug 30, 2015 at 15:11
  • @lago-lito: indeed it overwrites marks and buffers. For marks I never use all 26 marks in my buffers so I dont't think that's really a problem. For buffers it might be more of a problem, I think you can often find an unused buffer or if you really can't, use a temporary variable and the functions getreg() and setreg() to save your buffer. But I agree that it is not an optimal solution :-)
    – statox
    Aug 30, 2015 at 18:22
1

I've just seen another similar question and the solution consists of:

Jump to the middle plus one:

Mj

And run:

:,$g/./exe 'm' 2*line('.')-line('$')-1
1
  • Interesting :) Beware that this interleaves your whole file, though, and not only the selected paragraph!
    – iago-lito
    Sep 21, 2018 at 9:04
0

Lifting @dedowsdi's comment to a top-level answer:

Mark the first block, then run

:'<,'>g/^/'>+1m.
2
  • 1
    the ^ is not unneeded. Your version will act on whatever was the last used seach-pattern, while @dedowsdi comment would act on each line, by using the start-of-line regex atom. Dec 30, 2022 at 13:49
  • Oh, good point, fixed, thanks!
    – thakis
    Dec 31, 2022 at 14:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.