如何在Vim中合并两个多行文本块?

328

我想在Vim中合并两个块的行,即将行kl和行mn附加在一起。如果您喜欢伪代码解释:[line[k+i] + line[m+i] for i in range(min(l-k, n-m)+1)]

例如:

abc
def
...

123
45
...

应该变成什么?

abc123
def45

有没有一种不需要手动逐行复制粘贴的好方法来做这件事?


所以...你想加入交替行吗?也就是说,你想将第x行与第x+2行合并吗? - larsks
1
不,我有两个单独的块。伪代码:[a[i] + b[i] for i in min(len(a), len(b))] - ThiefMaster
2
请参考类似的问题(和答案!)这里 - NWS
10个回答

886

你可以使用块模式选择进行一次性复制/粘贴,但我猜这不是你想要的。

如果你想仅使用Ex命令来完成此操作

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

将会转换

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

进入

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

更新:获得这么多赞的答案应该有更详细的解释。

在 Vim 中,您可以使用管道字符(|)链接多个 Ex 命令,因此上面的命令等同于

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

许多 Ex 命令接受一系列行作为前缀参数 - 在上述情况中,del 前的 5,8s/// 前的 1,4 指定了命令操作的行。

del 删除给定的行。它可以接受寄存器参数,但当没有给出时,它会将行转储到未命名寄存器 @" 中,就像在正常模式下删除一样。然后,let l=split(@") 将已删除的行拆分成列表,使用默认分隔符:空白符。为了能够正确地处理已删除行中包含空格的输入,例如:

more than 
hour 
our 
never 
ever
after
work is
over
~

We'd需要指定不同的分隔符,以防止"work is"被分成两个列表元素:let l=split(@","\n")
最后,在替换s/$/\=remove(l,0)/中,我们将每行的结尾($)替换为表达式remove(l,0)的值。remove(l,0)会改变列表l,删除并返回其第一个元素。这样我们就可以按照读取它们的顺序替换已删除的行。我们也可以使用remove(l,-1)按相反的顺序替换已删除的行。

1
感谢您的解释。:sil5,8del | let l=split(@") | sil1,4s/$/\=remove(l,0)/ | call histdel("/", -1) | nohls 似乎更好,因为它在运行后清理搜索历史记录。而且它不会显示“x more/less lines”消息,需要我按回车键。 - ThiefMaster
12
如果您想要完整的 Vim 参考,请查看以下内容::help range:help :d:help :let:help split():help :s:help :s\=:help remove() - Benoit
17
确保像您这样的人想要发布这样的答案,这就是我成为版主的原因。干得好 :) - Tim Post
1
如果在前4个句子后面没有空格,那么就会出现问题。 - Reman
1
我不明白为什么这个答案有那么多赞。它是正确的,很卖萌,但对于实际生活和记住过程来说太高级了。只需记录一个简单的宏并重放即可。 - johannes_lalala
显示剩余6条评论

59

通过组合:global:move:join命令,可以获得一个优雅而简洁的Ex命令来解决这个问题。假设第一块行从缓冲区的第一行开始,并且光标位于第二块的第一行之前的行上,则命令如下。

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

如果需要详细解释此技术,请参见我的回答,针对一个与之基本相同的问题“如何在Vim中实现“paste -d '␣'”行为?”。”。


E16: 无效范围 - 但它仍然可以工作。当移除 1, 后,它可以正常运行而没有错误。 - ThiefMaster
很遗憾您不能接受多个答案 - 否则您的回答也会有一个绿色标记! - ThiefMaster
3
非常好!我之前不知道:move:join!的用法,也不了解''作为范围参数的含义(可以查看:help ''),以及+-作为范围修饰符的意思(可以查看:help range)。谢谢! - rampion
@ThiefMaster:该命令旨在用于将要连接的两个行范围相邻的情况下,以便光标最初位于紧接第二个块的第一行之前的第一个块的最后一行。 (请参见链接的答案和问题。)即使块中的行数不同,该命令也能按预期工作,尽管在这种情况下会显示错误消息。可以通过在命令前加上“sil!”来抑制错误消息。 - ib.
2
@ib.: 我认为把详细的解释也放到这个答案里是个好主意。 - ThiefMaster
如果光标在第一个块的最后一行,则可以使用相对地址到该块的第一行,例如:“:.-2,g / ^ /''+m。|-j!” - LXA

45

连接多行文本,需要按以下步骤进行:

  1. 跳转到第三行: jj
  2. 进入可视块模式: CTRL-v
  3. 将光标定位到行尾(对于长度不同的行非常重要): $
  4. 跳转到结尾: CTRL-END
  5. 剪切该块: x
  6. 跳转到第一行的结尾: kk$
  7. 在此处粘贴该块: p

这种移动方式可能不是最好的(我不是专家),但它可以像您想要的那样工作。希望有更简短的版本。

以下是使此技术有效的先决条件:

  • 起始块的所有行(例如问题中的abcdef)具有相同的长度XOR
  • 起始块的第一行是最长的,而你不关心其中的额外空格XOR
  • 起始块的第一行不是最长的,而且你想要额外的空格到末尾。

哇,那太有趣了!我从来没想过它会以那种方式工作。 - voithos
13
只有当“abc”和“def”的长度相同时,这个方法才能正常运行。块选择会保留已删除文本的缩进,因此如果光标停留在一行较短的文本上并插入了文本,则该文本会被插入到较长文本中字母之间,并且如果光标停留在较长文本上,则较短文本后面会添加空格。 - Izkata
确实... 有没有一种使用块复制粘贴的正确方法?毕竟这是最简单的方法(特别是如果由于某些原因无法在此处查找更复杂的方式)。 - ThiefMaster
2
就像@Izkata所说的那样,您会遇到在较长的行之间插入文本的问题。为了解决这个问题,只需在第一行末尾添加更多的空格,使其成为最长的行,然后粘贴您的文本块。完成后,将多个空格压缩为一个空格就像:%s/ \+/ /g这样简单。 - Khaja Minhajuddin
1
set ve=all 应该有所帮助,参见 http://vimdoc.sourceforge.net/htmldoc/options.html#'virtualedit' - Ben

19

这是我会如何操作(光标在第一行):

qama:5<CR>y$'a$p:5<CR>dd'ajq3@a

你需要知道两个东西:

  • 第二组的第一行从哪一行开始(在我的例子里是5),以及
  • 每个组有多少行(在我的例子中是3)。

这是正在发生的事情:

  • qa 记录下一个 q 的所有内容到名为 a 的“缓冲区”中。
  • ma 在当前行上创建一个标记。
  • :5<CR> 跳转到下一组。
  • y$ 复制剩余的行。
  • 'a 返回之前设置的标记。
  • $p 粘贴到该行的末尾。
  • :5<CR> 返回第二组的第一行。
  • dd 删除它。
  • 'a 返回之前的标记。
  • jq 向下移一行,并停止录制。
  • 3@a 重复该行动作(在我的例子中为3次)

1
你必须在两次输入:5后按下[Enter],否则这个操作将无法生效。 - Shawn J. Goff
1
我正在使用GVim。有没有办法在GVim中复制和粘贴命令?即使我当前处于正常(?)模式,^V也会自动粘贴到插入模式中(这很有道理,因为这通常是人们想要的)。我尝试过:norm qama:5<CR>y$'a$p:5<CR>dd'ajq3@a,但似乎只执行了q - ThiefMaster
1
ThiefMaster: 尝试使用 :let @a="ma:5^My$'a$p:5^Mdd'aj" | normal 4@a 命令,其中 ^M 字符是通过按下 CTRL-V 然后 Enter 键输入的。 - rampion

8

如其他地方所述,块选择是最好的方法。但您还可以使用以下任何变体:

:!tail -n -6%|粘贴-d'\0'%-| head -n 5

该方法依赖于UNIX命令行。 paste 实用程序是专门用于处理此类行合并的。

PASTE(1)                  BSD General Commands Manual                 PASTE(1)

NAME
     paste -- merge corresponding or subsequent lines of files

SYNOPSIS
     paste [-s] [-d list] file ...

DESCRIPTION
     The paste utility concatenates the corresponding lines of the given input files, replacing all but the last file's newline characters with a single tab character,
     and writes the resulting lines to standard output.  If end-of-file is reached on an input file while other input files still contain data, the file is treated as if
     it were an endless source of empty lines.

使用块选择并不是唯一的方法。也不是最简单的方法。可以通过短Vim命令实现所需的(类似于paste -d)行为,如我的答案所示。 - ib.
3
除此之外,我正在使用Windows操作系统,因此解决方案涉及在我的Linux机器上打开SSH连接,在编辑器中粘贴内容到终端中,然后再将结果复制回来。 - ThiefMaster

3

样本数据与rapion的相同。

:1,4s/$/\=getline(line('.')+4)/ | 5,8d

3

我认为不要把它弄得太复杂。 只需要打开虚拟编辑功能
(:set virtualedit=all)
选择方块123及其下面的所有内容。
然后将其放置在第一列之后即可。

abc    123
def    45
...    ...

并将两个以上的空格替换为一个空格:

:%s/\s\{2,}/ /g

问题实际上要求没有空格,我会做类似于 gvV:'<,'>s/\s+//g 的事情(vim 应该会自动为您插入 '<,'>,因此您不需要手动输入)。 - Ben

2
我会使用复杂的重复 :)
鉴于此:
aaa
bbb
ccc

AAA
BBB
CCC

将光标置于第一行,按照以下步骤操作:

qa}jdd''pkJxjq

然后按下@a(之后您可以随意使用@@)直到需要次数。

最终应该得到:

aaaAAA
bbbBBB
cccCCC

(加上一个换行符。)

解释:

  • qa 开始在 a 中录制复杂的重复操作。

  • } 跳到下一个空行。

  • jdd 删除下一行。

  • '' 回到最后一次跳转前的位置。

  • p 将删除的行粘贴在当前行下面。

  • kJ 将当前行添加到上一行的末尾。

  • x 删除 J 添加的组合行之间的空格;如果不需要空格,可以省略此步骤。

  • j 到下一行。

  • q 结束复杂的重复录制。

之后,您可以使用 @a 来运行存储在 a 中的复杂重复操作,然后可以使用 @@ 重新运行上次运行的复杂重复操作。


1

有很多方法可以实现这一点。我将使用以下两种方法之一合并两个文本块。

假设第一个块在第1行,第二个块从第10行开始,并且光标的初始位置在第1行。

(\n表示按下回车键。)

1. abc
   def
   ghi        

10. 123
    456
    789

使用copy、paste和join命令的宏。

qaqqa:+9y\npkJjq2@a10G3dd

使用移动第n行号和join命令的宏。

qcqqc:10m .\nkJjq2@c


0

@rampion的回答给了我灵感,我制作了一个键位映射来合并两个块

" paste copied block(already stored in register) to the end of current area
function block_paste(direct)
    let current_line = line('.')
    let copied_contents = split(@", "\n")

    for index in range(0, len(copied_contents) - 1)
        if a:direct == 'head' " head
            let res = copied_contents[index] . getline(current_line + index)
        elseif a:direct == 'tail' " tail
            let res = getline(current_line + index) . copied_contents[index]
        else
            let res = ''
        endif

        let target_line = current_line + index
        if target_line <= line('$')
            call setline(target_line, res)
        else
            call append(target_line - 1, res)
        endif
    endfor
endfunction


nnoremap <Leader>bp :call block_paste('tail')<CR>
nnoremap <Leader>bP :call block_paste('head')<CR>

按键映射 bp 可以将复制的块追加到当前块的末尾,而按键映射 bP 可以将复制的块插入到当前块的开头

如果您不喜欢 bp / bP,可以绑定到任何您喜欢的按键映射


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接