05 多文件打开与缓冲区:复制粘贴的正确姿势 你好,我是吴咏炜。

在前面的几讲里,我们介绍了 Vim 的基本命令和配置。有了这些基本功,单个文件的基本编辑对你来说应该已经不成问题了。不过,显然我们在工作和生活中不可能只用一个文件包打天下,你肯定还会遇到需要同时编辑多个文件的情况。今天,我们就来细细讨论一下这个话题,什么是编辑多个文件的正确姿势。

先来假设一个简单的使用场景,我们现在需要在某个目录下的所有 .cpp 和 .h 文件开头贴入一段版权声明,该如何操作?

单文件的打开方式

图形界面

使用图形界面的话,我们可以在操作系统的资源管理器里进入到合适的目录,然后逐个使用 Vim 来打开文件。我们可以使用右键菜单(“Edit with Vim”、“Open with…”等),也可以直接把文件拖拽到 Vim 里。使用“文件 > 打开”(File > Open)菜单当然也是一种选择,但这需要你记住上次打开到第几个文件,并不如使用资源管理器方便。

使用这几种编辑方式的话,你可以把需要粘贴的内容放到操作系统的剪贴板里,然后在图形界面的 Vim 里用以下方法之一粘贴进去(当然,如果光标不在开头的话,先用鼠标或用

gg 命令跳转到开头):

  • 正常模式 Vim 命令

“+P (意义我们后面再解释)

  • 快捷键
(提醒:这是我们对 ⌘V 的标记方式;仅适用于 macOS)或 (PC 键盘) * 鼠标右键加“粘贴”(Paste) * 菜单“编辑 > 粘贴”(Edit > Paste) 注意,如果你通常使用 Ctrl-V 键粘贴的话,这个快捷键在 Vim 里并不适用。即使你使用的是图形界面的 Vim 也是如此,因为这个键在 Vim 里有其他用途。顺便说一句,这个键在 Unix 终端上也一样是不能用作粘贴的。 显然,在远程连接到服务器上时,以上方法不可用,我们得考虑终端 Vim 的用法。 ### 终端 Vim 如果直接把图形界面下的基本步骤,翻译成终端 Vim(非图形界面)的用法的话,应该是这样子的: * 在终端里进入到目标目录下 * 使用 vim 文件名 来逐一打开需要编辑的文件 * 如果光标不在开头的话,用鼠标或 gg 命令跳转到开头 * 使用命令 i 进入插入模式 * 使用终端窗口的粘贴命令或快捷键(如 )来粘贴内容 * 按 回到正常模式并用 ZZ 存盘退出 或者,我们还可以采用下面的不退出 Vim 的处理方法: * 打开文件使用 :e 文件名 ;可以使用 来查看有哪些文件,及用 进行自动完成 * 存盘使用 :w 但是如果粘贴的内容含缩进、而 Vim 又不够新的话,我们还会有特殊的麻烦。请继续往下看。 ### Vim 老版本的特殊处理 ![Fig5.1](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Vim%20%e5%ae%9e%e7%94%a8%e6%8a%80%e5%b7%a7%e5%bf%85%e7%9f%a5%e5%bf%85%e4%bc%9a/assets/4b68d006f7f3b2003700b3001fb48f0c.png "老版本 Vim 下直接粘贴可能出现的错误结果") 上面的图片展示了 Vim 用户可能遇到的一种错误情况。这是因为对于终端 Vim 来说,一般而言,它是没法分辨用户输入和粘贴的。因此,在粘贴内容时,Vim 的很多功能,特别是和自动缩进相关的,就会和输入打架,导致最后的结果不对。 要解决这个问题,你就得让 Vim 知道,你到底是在输入还是在粘贴。Vim 有一个 paste 选项,就是用来切换输入/粘贴状态的。如果这个选项打开的话( :set paste ),Vim 就认为你在粘贴,智能缩进、制表符转换等功能就不会修改粘贴的内容。 不过,手工设置该选项(及事后用 set nopaste 取消)是件烦人的事。所幸 xterm 里有一个“括号粘贴模式”(bracketed paste mode)可以帮 Vim 判断目前是输入还是粘贴。这个模式启用后,终端在发送剪贴板的内容之前和之后都会发送特殊的控制字符序列,来通知应用程序进行特殊的处理。 启用括号粘贴模式需要向 xterm 发送启用序列 [?2004h ,关闭括号粘贴模式需要向 xterm 发送关闭序列 [?2004l ;在启用了括号粘贴模式后,xterm 在发送剪贴板内容时会在前后分别加上开始粘贴序列 [200~ 和结束粘贴序列 [201~ 。 Vim 8.0.0210 开始引入了对括号粘贴模式的支持。在兼容 xterm 的终端里进行粘贴时,你不再需要使用 paste 这个选项了。更棒的是,目前你甚至都不需要进入插入模式就可以粘贴了——这是不是就方便多了? 如果你使用的是 Vim 8.0.0210 之前的版本的话,那我们至少也可以通过代码来使得手工设置 paste 选项变得不必要。你可以在 vimrc 里加入下面的代码: if !has('patch-8.0.210') " 进入插入模式时启用括号粘贴模式 let &t_SI .= "\[?2004h" " 退出插入模式时停用括号粘贴模式 let &t_EI .= "\[?2004l" " 见到 [200~ 就调用 XTermPasteBegin inoremap [200~ XTermPasteBegin() function! XTermPasteBegin() " 设置使用 [201~ 关闭粘贴模式 set pastetoggle=[201~ " 开启粘贴模式 set paste return "" endfunction endif 这个功能虽然小,但解决了在远程连接上使用 Vim 粘贴代码的一个常见烦恼。因此,我认为你需要了解一下。 ### “已经存在交换文件!” 对每个文件单独使用一个 Vim 会话来编辑,很容易出现冲突的情况,所以你迟早会遇到“已经存在交换文件!”(Swap file “…” already exists!)的错误提示。出现这个提示,有两种可能的原因: * 你上次编辑这个文件时,发生了意外崩溃。 * 你已经在使用另外一个 Vim 会话编辑这个文件了。 原因不同,我们处理的策略自然也不相同。当进程 ID(process ID)后面没有“STILL RUNNING”这样的字样时,那就是情况 1;否则,就是情况 2 了。 ![Fig5.2](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Vim%20%e5%ae%9e%e7%94%a8%e6%8a%80%e5%b7%a7%e5%bf%85%e7%9f%a5%e5%bf%85%e4%bc%9a/assets/d09c693ab664af0853139d8f04eedd27.png "上次编辑这个文件时发生了意外崩溃的错误提示") 上图中没有“STILL RUNNING”的字样,说明是情况 1。这时你需要按 r 来恢复上次的编辑状态——Vim 支持即使在你没有存盘的情况下仍然保存你的编辑状态,因而这种方法可以恢复你上次没有存盘的内容。 需要注意的是,在恢复之后,Vim 仍然不会删除崩溃时保留下来的那个交换文件。因此,在确定内容无误、保存文件之后,你需要重新再打开文件,并按 d 键把交换文件删除。当然,如果你确定目前保存的文件版本就是你想要的,也可以直接按 d 把交换文件删除、重新编辑文件。 反过来,如果你已经在另一个 Vim 会话里编辑文件的话,我们就会在进程 ID 后面看到“STILL RUNNING”的字样;同时,Vim 界面上也没有了删除(Delete)交换文件这一选项。 ![Fig5.3](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Vim%20%e5%ae%9e%e7%94%a8%e6%8a%80%e5%b7%a7%e5%bf%85%e7%9f%a5%e5%bf%85%e4%bc%9a/assets/a45521128650b2824381de6f84911b6f.png "文件正在其他地方被编辑的错误提示") 这时,大部分情况下我们应当使用 q 或 a (绝大部分情况下没有区别)放弃编辑,并找到目前已经打开的 Vim 窗口,从那里继续。少数情况下,我们只是要查看文件,那也可以选择 o 只读打开文件。需要使用 e 强行编辑的情况很少,需要非常谨慎——比如,你确认另外有 Vim 会话,但里面不会去做任何修改,这是我目前想得出来的唯一的合理需求。 如果我们使用图形界面 Vim 8 的话,Vim 支持在文件已经打开时自动切换到已经打开的 Vim 窗口上。这个功能在文件处于一个不活跃的标签页(下一讲会讨论标签页支持)时特别有用,因为 Vim 能把这个标签页自动切到最前面。不过,这个功能不是默认激活的,我们需要在 vimrc 中加入以下内容: if v:version >= 800 packadd! editexisting endif 好了,目前我们已经讨论了最简单、无聊、低效的工作方式。可以明显看到,不管是使用图形界面 Vim,还是终端 Vim,上面的方法本质上把 Vim 当成了记事本来用,完全没有体现出任何高效性或方便性。 既然使用号称“高效”的 Vim,我们当然就得有更加高效的做法。下面,我们以多文件打开为例加以说明。 ## 多文件的打开方式 首先,我们需要知道,Vim 支持一次性打开多个文件,你只需要在命令行上写出多个文件即可,或者使用通配符。比如,就我们刚才所说的编辑场景,我们可以使用 vim /*.cpp /*.h 。 有可能让你吃惊的是,输入这个命令之后,Vim 只打开了一个文件,那就是所有文件中的第一个。 原来,为了确保在配置较差的环境里仍然能够正常工作,Vim 绝对不会不必要地消耗内存,包括打开不必要立即打开的文件。所以在上面的命令后,Vim 建立了一个文件列表,并且暂时只打开其中的第一个文件。接下来,用户可以决定,要编辑哪个文件,或者查看列表,或者提前退出,等等。 为此,Vim 提供了以下命令: * :args :可以显示“参数”,即需要编辑的多个文件的列表 * :args 文件名 :使用新的文件名替换参数列表 * :next (可缩写为 :n ):打开下一个文件;如当前文件修改(未存盘)则会报错中止,但如果命令后面加 ! 则会放弃修改内容,其他命令也类似 * :Next (缩写 :N )或 :previous (缩写 :prev ):打开上一个文件 * :first 或 :rewind :回到列表中的第一个文件 * :last :打开列表中的最后一个文件 使用这些命令,我们的工作流当然就会发生变化了: * 在终端里进入到目标目录下 * 使用 vim /*.cpp /*.h 或 gvim /*.cpp /*.h 来打开需要编辑的文件 * 对于第一个文件,使用之前的方法贴入所需的文本 * 使用 V 进入行选择的可视模式,移动光标选中所需的文本,然后使用 y 复制选中的各行 * 执行命令 :set autowrite ,告诉 Vim 在切换文件时自动存盘 * 执行命令 :n|normal ggP ,切换到下一个文件并执行正常模式命令 ggP ,跳转到文件开头并贴入文本 * 确认修改无误后,键入 : 、上箭头和回车,重复执行上面的命令 * 待 Vim 报错说已经在最后一个文件里,使用 :w 存盘,或 :wq (抑或更快的 ZZ )存盘退出 注意,第 6 步可以拆成 :n 和 ggP 两步,但文件数量较多时,反复手工敲 ggP 也挺累的。因此,我这儿使用了 normal 命令,在命令行模式下执行正常模式命令,下面就可以直接重复切换命令加粘贴命令,我们的编辑效率也得以大大提升。 ![Fig5.4](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Vim%20%e5%ae%9e%e7%94%a8%e6%8a%80%e5%b7%a7%e5%bf%85%e7%9f%a5%e5%bf%85%e4%bc%9a/assets/7b2341087d8a34471b9c1979acdcddcd.gif "第 4 步到第 7 步的演示(注意倒数第二行的变化)") 这种编辑方式,是不是就比之前的优越多了? 另外,Vim 还能解决一个 shell 相关的不一致性问题。如果我们要编辑的文件除了当前目录下的,还有所有子目录下的,在大部分 shell 下,包括 Linux 上缺省的 Bash,我们需要使用“/*.cpp /*.h /**/*.cpp **//*.h”来挑选这些文件,重复、麻烦。Vim 在此处采用了类似于 Zsh 的简化语法,“**”也包含了当前目录。这样,我们只需把上面第 2 步改成下面这样即可: * 键入 vim 进入 Vim,然后使用 :args /*/*//*.cpp /*/*//*.h 来打开需要编辑的文件 ### 缓冲区的管理和切换 跟多文件相关又略微不同的一个概念是缓冲区(buffer)。它是 Vim 里的一个基本概念,和今天讲的很多其他内容有相关性和相似性,你也或迟或早终究会遇到它,我今天也一起概要描述一下。 Vim 里会对每一个已打开或要打开的文件创建一个缓冲区,这个缓冲区就是文件在 Vim 中的映射。在多文件编辑的时候你也会有同样数量的缓冲区。不过,缓冲区的数量常常会更高,因为你用 :e 等命令打开的文件不会改变“命令行参数”(只被命令行或 :args 命令修改),但同样会增加缓冲区的数量。 此外, :args 代表参数列表/文件列表,真的只是文件的列表而已。缓冲区中有更多信息的,最最基本的就是记忆了光标的位置。在 Vim 里,除了切换到下一个文件这样的批处理操作外,操作缓冲区的命令比简单操作文件的命令更为方便。 作为对比,我们来看一下文件列表和缓冲区列表的命令的结果。 ![Fig5.5](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Vim%20%e5%ae%9e%e7%94%a8%e6%8a%80%e5%b7%a7%e5%bf%85%e7%9f%a5%e5%bf%85%e4%bc%9a/assets/0ff5249a78179117dfcda70715dc3aa4.png "文件列表命令 :args 的结果") ![Fig5.6](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Vim%20%e5%ae%9e%e7%94%a8%e6%8a%80%e5%b7%a7%e5%bf%85%e7%9f%a5%e5%bf%85%e4%bc%9a/assets/3395a72ee4776c49748243fac411e008.png "缓冲区列表命令 :ls 的结果") 可以看到,两者都展示了文件,都标示出了当前编辑的文件(分别使用方括号和“%a”)。不过,缓冲区列表中明显有更多的信息: * 文件名前面有编号;我们也马上就会说到利用编号的命令。 * 除了当前活跃文件的标记“%a”,还有个文件被标成了“/#”,这表示最近的缓冲区;缓冲区列表里还可能有其他标记,如“+”表示缓冲区已经被修改。 * 文件名后面有行号,表示光标在文件中的位置。 常用的缓冲区命令跟前面文件列表相关的命令有很大的相似性,因此我在这儿一起讲,可以帮助你记忆: * :buffers 或 :ls :可以显示缓冲区的列表 * :buffer 缓冲区列表里的编号 ( :buffer 可缩写为 :b ):跳转到编号对应的缓冲区;如当前缓冲区已被修改(未存盘)则会报错中止,但如果命令后面加 ! 则会放弃修改内容;其他命令也类似 * :bdelete 缓冲区列表里的编号 ( :bdelete 可缩写为 :bd ):删除编号对应的缓冲区;编号省略的话删除当前缓冲区 * :bnext (缩写 :bn ):跳转到下一个缓冲区 * :bNext (缩写 :bN )或 :bprevious (缩写 :bp ):跳转到上一个缓冲区 * :bfirst 或 :brewind :跳转到缓冲区列表中的第一个文件 * :blast :跳转到缓冲区列表中的最后一个文件 还有很常见的一种情况是,我们需要在两个文件之间切换。Vim 对最近编辑的文件(上面提到的列表里标有“/#”的文件)有特殊的支持,使用快捷键 <C-^> 可以在最近的两个缓冲区之间来回切换。这个快捷键还有一个用法是在前面输入缓冲区的编号:比如,用 1<C-^> 可以跳转到第一个缓冲区(跟命令行模式的命令 :bfirst 或 :b1 效果相同)。 从实际使用的角度,使用缓冲区列表有点像打开最近使用的文件菜单(但缓冲区列表不会存盘),可以当作一种快速切换到最近使用的文件的方式。 缓冲区是文件在某个 Vim 会话里的映射。这意味着,如果某个 Vim 会话里不同的窗口或标签页(下一讲里会讨论)编辑的是同一个文件,它们对应到的也会是同一个缓冲区。更重要的是,文件/缓冲区的修改在同一个 Vim 会话里是完全同步的——这就不会像在多会话编辑时那样发生冲突和产生错误了。 ## 内容小结 本讲通过讨论使用 Vim 在多个文件里粘贴代码的多种方法,我们学习了以下知识: * 在图形界面和终端里,粘贴系统剪贴板的内容需要使用不同的方法:前者使用 Vim 命令,后者则需进入插入模式,使用终端的粘贴命令进行粘贴 * Vim 能在崩溃后恢复未存盘的内容,也能在多会话编辑同一个文件时检测到这种冲突 * 在 Vim 里我们可以使用通配符“/*.后缀”和“/**/*.后缀”来打开多个文件 * 使用 :args 命令我们可以展示或替换参数列表,使用 :next 等命令我们可以在这些参数指定的文件中切换 * 使用 :buffers 或 :ls 命令我们可以展示缓冲区列表,即所有已编辑和将编辑的文件,使用 :b 和 :bnext 等命令我们可以在这些缓冲区中进行切换 今天讲到了一些命令行模式的命令,你应该可以看到,它们都是非常有规律的,最基本的操作就是“first”、“last”、“next”、“Next” 或 “previous”等英文单词,以及它们与前缀的组合。把命令行模式的命令记住,就能完成基本的编辑任务;至于像 <C-^> 这样的正常模式命令,万一记不住,也可以用命令行模式的命令来替代。但是,正常模式的命令更加高效,有助于提高你的编辑效率,所以最好通过多加练习来形成“肌肉记忆”。 对于配置文件,本讲只有很小的更改,对应的标签是 l5-unix 和 l5-windows 。 ## 课后练习 请在课后进行以下练习,熟悉今天所讲的内容: * 用 Vim 打开一个文件,进行编辑(不存盘),然后将这个 Vim 进程 kill 掉;重新打开文件,恢复其中内容并存盘;再次打开文件,删除交换文件。 * 用 Vim 打开一个文件,然后在另外一个终端窗口里再次打开这个文件,阅读冲突信息,然后退出编辑。 * 使用 Vim 打开多个文件,逐个查看,然后退出。 我是吴咏炜,我们下一讲再见! # 参考资料 https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Vim%20%e5%ae%9e%e7%94%a8%e6%8a%80%e5%b7%a7%e5%bf%85%e7%9f%a5%e5%bf%85%e4%bc%9a/05%20%e5%a4%9a%e6%96%87%e4%bb%b6%e6%89%93%e5%bc%80%e4%b8%8e%e7%bc%93%e5%86%b2%e5%8c%ba%ef%bc%9a%e5%a4%8d%e5%88%b6%e7%b2%98%e8%b4%b4%e7%9a%84%e6%ad%a3%e7%a1%ae%e5%a7%bf%e5%8a%bf.md * any list {:toc}