3

I need to restructure a markdown table, and for that, I wish there was a quick way to "join" lines from a multi-line cell.

So with Vim, I would have to yank the block (Ctrl-v), and then paste it as a single line.

Is there a convenient key mapping for doing that?

Before:

chr6_66364     1.846        VNN3           chr6     132967948    132968077 
                            VNN1           
                            VNN2           
                            STX7           
                            CTGF           
                            RPS12          
                            MOXD1          
                            SLC18B1        
chr17_12565    1.846        DDX52          chr17    35639584     35639601

After:

chr6_66364     1.846        VNN3 VNN1 VNN2 STX7 CTGF RPS12 MOXD1 SLC18B1           chr6     132967948    132968077
chr17_12565    1.846        DDX52          chr17    35639584     35639601

which required me to:

  1. join lines 2 to 8 (line selection + J);
  2. Substitute multiple spaces into a single one (:'<,'>s/ \+/ /g);
  3. delete and paste into line 1, column 3.

(Also, as step 0, the other columns are initially not empty, so I have to block select them and replace by space (Ctrl-v + r + space).

  • 2
    For clarity, can you post a before/after example of what you want? There may also be a better way to achieve your goal than pasting block-selection as a single line. – Martin Tournoij Jun 17 at 9:04
  • 1
    To corroborate Martin's comment see What is the XY problem – statox Jun 17 at 9:14
4

You can use setreg() to manipulate the contents of the register, after you delete into it from the visual block. Note that setreg() takes an optional third argument that you can use to set the "type" of the register, where you can pass 'c' or 'v' to make it a "characterwise" mode register.

Assuming you deleted your block into the default register, you can then use this command to join the lines in the register contents:

 :call setreg('', substitute(@", '\s*\n', ' ', 'g'), 'c')

You can then simply paste it with P, as you'd expect.

If you would like to turn that into a reusable command, then consider something like the below, which you can add to your vimrc or to a plug-in file:

command! -register -bar JoinLines
  \ call setreg('<reg>', substitute(getreg('<reg>'), '\s*\n', ' ', 'g'), 'c')

Then you'll be able to use :JoinLines to join the lines of the default register, or pass a different register as an optional argument with :JoinLines a.

| improve this answer | |
  • I'm not sure if it's a typo or me missing something in the help but I think in your first command your first argument to setreg() should be '"' and not '', right? – statox Jun 17 at 12:37
  • @statox It's actually documented in the second line in help: If {regname} is "" or "@", the unnamed register '"' is used. – filbranden Jun 17 at 12:38
  • Ah it's a difference between neovim's help and vim's one :h setreg() – statox Jun 17 at 13:01
  • @statox I wonder why they would touch this part of the help text... 🤷‍♂️ – filbranden Jun 17 at 13:12
  • 3
    @filbranden Neovim just lacks behind the official Vim runtime. The mentioned paragraph was added as of github.com/vim/vim/commit/… – Christian Brabandt Jun 17 at 13:30
4

Here is one way to achieve what you want to do. I think another solution could be to use a macro but getting back at the right position might not always be trivial.

So once you yanked your column in visual block mode, the first step is to get back a list containing every lines. To do that you can use :h getreg() like this:

getreg('"', 1, 1)

The first argument '"' describes the register you want to get, here we use the unnamed register as this is the one used by default when you yank some text. The second argument only has an effect when the first parameter is the expression register '=' which is not the case here. The third argument is used to get a list instead of a string. This command returns something like this:

['VNN3   ', 'VNN1   ', 'VNN2   ', 'STX7   ', 'CTGF   ', 'RPS12  ', 'MOXD1  ', 'SLC18B1']

Now we want to get this list as a string with at least one space separating each item. We can do that with :h join():

join(getreg('"', 1, 1), ' ')

Now we have this:

VNN3    VNN1    VNN2    STX7    CTGF    RPS12   MOXD1   SLC18B1

Let's then remove the redundant white spaces with :h substitute():

substitute(join(getreg('"', 1, 1), ' '), '\s\+', ' ', 'g')

Looks good:

VNN3 VNN1 VNN2 STX7 CTGF RPS12 MOXD1 SLC18B1

Finally we want to add the result of this command in the current buffer. To do so we can use the expression register :h quote=. In insert mode you will need to type Ctrl+r to start inserting the content of a register. Then type = to select the expression register and write the command we used before to insert the text.

All of this is nice but it would be better to have a mapping to do all the typing for us. The following line added to your vimrc should do the trick:

nnoremap <YOUR_KEY> i<C-r>=substitute(join(getreg('"', 1, 1), ' '), '\s\+', ' ', 'g')<CR>

Now all you have to do is yank your column in visual block mode and press <YOUR_KEY> in normal mode to get the result inserted after your cursor.

EDIT 2 as Ben pointed out in the comments, using map to trim the strings instead of using a substitution is shorter and more elegant see the relevant doc :h map() (There might even be a way to improve the lambda used by map but I don't know how and don't want to spend more time on that :) ):

join(map(getreg('"', 1, 1), {_, v -> trim(v)}), ' ')

EDIT 1 Just because it's fun I tried to do it with a macro. The idea is to create a repeatable sequence of key presses to get your result.

First I did the assumption that your text looks like this:

chr6_66364     1.846        VNN3           chr6     132967948    132968077
chr6_66364     1.846        VNN1           chr6     132967948    132968077 
chr6_66364     1.846        VNN2           chr6     132967948    132968077 
chr6_66364     1.846        STX7           chr6     132967948    132968077 
chr6_66364     1.846        CTGF           chr6     132967948    132968077 
chr6_66364     1.846        RPS12          chr6     132967948    132968077 
chr6_66364     1.846        MOXD1          chr6     132967948    132968077 
chr6_66364     1.846        SLC18B1        chr6     132967948    132968077 

You should start with your cursor on the 3 of VNN3.

Start recording a macro with qq for example. Then press the following keys (Note ^[ is actually the escape key):

m'j02Wyiw``a ^[pm'jdd``

And when you are done press q again to stop recording the macro. Here is what you have done:

m'                      Create a mark we will use later to go back to the current position
  j0                    Go down a line and go at the beggining of the line
    2W                  Go the the column you want to merge
      yiw               Copy the word in the column
         ``             Go back to the exact position of the mark we created in the first step
           a            Start insert mode
                        Add a litteral whitespace
             ^[         This is actually the Escape character to go back to normal mode
               p        Paste the word you yanked before
                m'      Set the new position of the mark.
                  jdd   Go down one line and delete the line
                     `` Go back to the end of the recently pasted word

Now you can simply press @q to play back the macro and get a new line joined with every execution.

As I said at the beginning of my answer I think this is more obscure and error prone than my first solution.

| improve this answer | |
  • 1
    Personally, i would remove the spaces with map/trim or similar before joining them, but i’m impressed! – D. Ben Knoble Jun 17 at 16:05
  • @D.BenKnoble That would indeed be more elegant, I'll add that to the answer :) – statox Jun 17 at 16:21
1

Not sure if the pattern is true for your entire file, but your example can be interpreted as: "For all lines beginning with whitespace, replace the preceding whitespace, linebreak and beginning whitespace with a single space" which can be done with

:%s/\s*\n\s\+/ \1/g

@filbranden correctly pointed out the shortcomings of this single substitution. To produce exactly the formatting asked for can be done with 3 such:

%s/\s*\n\s\+/ \1/ge | %s/\(\%(\S\+\s\+\)\{3}\)\(\%(\S\+\s\+\)\{3}\)\(.*\)/\1\3\2/e | %s/^\(\%(\s*\S\+\)\{3}\)\s\+\(.*\)/\1 \2/e

but if you are not fussy about having the whitespace exactly so, you can drop the 3rd substitution command.

| improve this answer | |
  • This doesn't work perfectly for the OP use case, since the first line actually has a 4th, 5th and 6th fields and it's trying to join the contents of the 3rd field only while preserving the ones that follow it... The solution you propose would concatenate the following lines to the end of the first line, after the first field. – filbranden Jun 18 at 1:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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