<audio id="audio" title="05｜多文件打开与缓冲区：复制粘贴的正确姿势" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9d/80/9da336597c9e732028d244089e8fb880.mp3"></audio>

你好，我是吴咏炜。

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

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

## 单文件的打开方式

### 图形界面

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

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

- 正常模式 Vim 命令 `"+P`（意义我们后面再解释）
- 快捷键 `&lt;D-V&gt;`（提醒：这是我们对 ⌘V 的标记方式；仅适用于 macOS）或 `&lt;S-Insert&gt;`（PC 键盘）
- 鼠标右键加“粘贴”（Paste）
- 菜单“编辑 &gt; 粘贴”（Edit &gt; Paste）

注意，如果你通常使用 Ctrl-V 键粘贴的话，这个快捷键在 Vim 里并不适用。即使你使用的是图形界面的 Vim 也是如此，因为这个键在 Vim 里有其他用途。顺便说一句，这个键在 Unix 终端上也一样是不能用作粘贴的。

显然，在远程连接到服务器上时，以上方法不可用，我们得考虑终端 Vim 的用法。

### 终端 Vim

如果直接把图形界面下的基本步骤，翻译成终端 Vim（非图形界面）的用法的话，应该是这样子的：

1. 在终端里进入到目标目录下
1. 使用 `vim 文件名` 来逐一打开需要编辑的文件
1. 如果光标不在开头的话，用鼠标或 `gg`  命令跳转到开头
1. 使用命令 `i` 进入插入模式
1. 使用终端窗口的粘贴命令或快捷键（如 `&lt;S-Insert&gt;`）来粘贴内容
1. 按 `&lt;Esc&gt;` 回到正常模式并用 `ZZ` 存盘退出

或者，我们还可以采用下面的不退出 Vim 的处理方法：

- 打开文件使用 `:e 文件名`；可以使用 `&lt;C-D&gt;` 来查看有哪些文件，及用 `&lt;Tab&gt;` 进行自动完成
- 存盘使用 `:w`

但是如果粘贴的内容含缩进、而 Vim 又不够新的话，我们还会有特殊的麻烦。请继续往下看。

#### Vim 老版本的特殊处理

<img src="https://static001.geekbang.org/resource/image/4b/0c/4b68d006f7f3b2003700b3001fb48f0c.png" alt="Fig5.1" title="老版本 Vim 下直接粘贴可能出现的错误结果">

上面的图片展示了 Vim 用户可能遇到的一种错误情况。这是因为对于终端 Vim 来说，一般而言，它是没法分辨用户输入和粘贴的。因此，在粘贴内容时，Vim 的很多功能，特别是和自动缩进相关的，就会和输入打架，导致最后的结果不对。

要解决这个问题，你就得让 Vim 知道，你到底是在输入还是在粘贴。Vim 有一个 `paste` 选项，就是用来切换输入/粘贴状态的。如果这个选项打开的话（`:set paste`），Vim 就认为你在粘贴，智能缩进、制表符转换等功能就不会修改粘贴的内容。

不过，手工设置该选项（及事后用 `set nopaste` 取消）是件烦人的事。所幸 xterm 里有一个“括号粘贴模式”（bracketed paste mode）可以帮 Vim 判断目前是输入还是粘贴。这个模式启用后，终端在发送剪贴板的内容之前和之后都会发送特殊的控制字符序列，来通知应用程序进行特殊的处理。

启用括号粘贴模式需要向 xterm 发送启用序列 `&lt;Esc&gt;[?2004h`，关闭括号粘贴模式需要向 xterm 发送关闭序列 `&lt;Esc&gt;[?2004l`；在启用了括号粘贴模式后，xterm 在发送剪贴板内容时会在前后分别加上开始粘贴序列 `&lt;Esc&gt;[200~` 和结束粘贴序列 `&lt;Esc&gt;[201~`。

Vim 8.0.0210 开始引入了对括号粘贴模式的支持。在兼容 xterm 的终端里进行粘贴时，你不再需要使用 `paste` 这个选项了。更棒的是，目前你甚至都不需要进入插入模式就可以粘贴了——这是不是就方便多了？

如果你使用的是 Vim 8.0.0210 之前的版本的话，那我们至少也可以通过代码来使得手工设置 `paste` 选项变得不必要。你可以在 vimrc 里加入下面的代码：

```
if !has('patch-8.0.210')
  " 进入插入模式时启用括号粘贴模式
  let &amp;t_SI .= "\&lt;Esc&gt;[?2004h"
  " 退出插入模式时停用括号粘贴模式
  let &amp;t_EI .= "\&lt;Esc&gt;[?2004l"
  " 见到 &lt;Esc&gt;[200~ 就调用 XTermPasteBegin
  inoremap &lt;special&gt; &lt;expr&gt; &lt;Esc&gt;[200~ XTermPasteBegin()

  function! XTermPasteBegin()
    " 设置使用 &lt;Esc&gt;[201~ 关闭粘贴模式
    set pastetoggle=&lt;Esc&gt;[201~
    " 开启粘贴模式
    set paste
    return ""
  endfunction
endif

```

这个功能虽然小，但解决了在远程连接上使用 Vim 粘贴代码的一个常见烦恼。因此，我认为你需要了解一下。

#### “已经存在交换文件！”

对每个文件单独使用一个 Vim 会话来编辑，很容易出现冲突的情况，所以你迟早会遇到“已经存在交换文件！”（Swap file “…” already exists!）的错误提示。出现这个提示，有两种可能的原因：

1. 你上次编辑这个文件时，发生了意外崩溃。
1. 你已经在使用另外一个 Vim 会话编辑这个文件了。

原因不同，我们处理的策略自然也不相同。当进程 ID（process ID）后面没有“STILL RUNNING”这样的字样时，那就是情况 1；否则，就是情况 2 了。

<img src="https://static001.geekbang.org/resource/image/d0/27/d09c693ab664af0853139d8f04eedd27.png" alt="Fig5.2" title="上次编辑这个文件时发生了意外崩溃的错误提示">

上图中没有“STILL RUNNING”的字样，说明是情况 1。这时你需要按 `r` 来恢复上次的编辑状态——Vim 支持即使在你没有存盘的情况下仍然保存你的编辑状态，因而这种方法可以恢复你上次没有存盘的内容。

需要注意的是，在恢复之后，Vim 仍然不会删除崩溃时保留下来的那个交换文件。因此，在确定内容无误、保存文件之后，你需要重新再打开文件，并按 `d` 键把交换文件删除。当然，如果你确定目前保存的文件版本就是你想要的，也可以直接按 `d` 把交换文件删除、重新编辑文件。

反过来，如果你已经在另一个 Vim 会话里编辑文件的话，我们就会在进程 ID 后面看到“STILL RUNNING”的字样；同时，Vim 界面上也没有了删除（Delete）交换文件这一选项。

<img src="https://static001.geekbang.org/resource/image/a4/6f/a45521128650b2824381de6f84911b6f.png" alt="Fig5.3" title="文件正在其他地方被编辑的错误提示">

这时，大部分情况侠我们应当使用 `q` 或 `a`（绝大部分情况下没有区别）放弃编辑，并找到目前已经打开的 Vim 窗口，从那里继续。少数情况下，我们只是要查看文件，那也可以选择 `o` 只读打开文件。需要使用 `e` 强行编辑的情况很少，需要非常谨慎——比如，你确认另外有 Vim 会话，但里面不会去做任何修改，这是我目前想得出来的唯一的合理需求。

如果我们使用图形界面 Vim 8 的话，Vim 支持在文件已经打开时自动切换到已经打开的 Vim 窗口上。这个功能在文件处于一个不活跃的标签页（下一讲会讨论标签页支持）时特别有用，因为 Vim 能把这个标签页自动切到最前面。不过，这个功能不是默认激活的，我们需要在 vimrc 中加入以下内容：

```
if v:version &gt;= 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`：打开列表中的最后一个文件

使用这些命令，我们的工作流当然就会发生变化了：

1. 在终端里进入到目标目录下
1. 使用 `vim *.cpp *.h` 或 `gvim *.cpp *.h` 来打开需要编辑的文件
1. 对于第一个文件，使用之前的方法贴入所需的文本
1. 使用 `V` 进入行选择的可视模式，移动光标选中所需的文本，然后使用 `y` 复制选中的各行
1. 执行命令 `:set autowrite`，告诉 Vim 在切换文件时自动存盘
1. 执行命令 `:n|normal ggP`，切换到下一个文件并执行正常模式命令 `ggP`，跳转到文件开头并贴入文本
1. 确认修改无误后，键入 `:`、上箭头和回车，重复执行上面的命令
1. 待 Vim 报错说已经在最后一个文件里，使用 `:w` 存盘，或 `:wq`（抑或更快的 `ZZ`）存盘退出

注意，第 6 步可以拆成 `:n` 和 `ggP` 两步，但文件数量较多时，反复手工敲 `ggP` 也挺累的。因此，我这儿使用了 `normal` 命令，在命令行模式下执行正常模式命令，下面就可以直接重复切换命令加粘贴命令，我们的编辑效率也得以大大提升。

<img src="https://static001.geekbang.org/resource/image/7b/cd/7b2341087d8a34471b9c1979acdcddcd.gif" alt="Fig5.4" title="第 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 里，除了切换到下一个文件这样的批处理操作外，操作缓冲区的命令比简单操作文件的命令更为方便。

作为对比，我们来看一下文件列表和缓冲区列表的命令的结果。

<img src="https://static001.geekbang.org/resource/image/0f/a4/0ff5249a78179117dfcda70715dc3aa4.png" alt="Fig5.5" title="文件列表命令 :args 的结果">

<img src="https://static001.geekbang.org/resource/image/33/08/3395a72ee4776c49748243fac411e008.png" alt="Fig5.6" title="缓冲区列表命令 :ls 的结果">

可以看到，两者都展示了文件，都标示出了当前编辑的文件（分别使用方括号和“%a”）。不过，缓冲区列表中明显有更多的信息：

- 文件名前面有编号；我们也马上就会说到利用编号的命令。
- 除了当前活跃文件的标记“%a”，还有个文件被标成了“#”，这表示最近的缓冲区；缓冲区列表里还可能有其他标记，如“+”表示缓冲区已经被修改。
- 文件名后面有行号，表示光标在文件中的位置。

常用的缓冲区命令跟前面文件列表相关的命令有很大的相似性，因此我在这儿一起讲，可以帮助你记忆：

- `:buffers` 或 `:ls`：可以显示缓冲区的列表
- `:buffer 缓冲区列表里的编号`（`:buffer` 可缩写为 `:b`）：跳转到编号对应的缓冲区；如当前缓冲区已被修改（未存盘）则会报错中止，但如果命令后面加 `!` 则会放弃修改内容；其他命令也类似
- `:bdelete 缓冲区列表里的编号`（`:bdelete` 可缩写为 `:bd`）：删除编号对应的缓冲区；编号省略的话删除当前缓冲区
- `:bnext`（缩写 `:bn`）：跳转到下一个缓冲区
- `:bNext`（缩写 `:bN`）或 `:bprevious`（缩写 `:bp`）：跳转到上一个缓冲区
- `:bfirst` 或 `:brewind`：跳转到缓冲区列表中的第一个文件
- `:blast`：跳转到缓冲区列表中的最后一个文件

还有很常见的一种情况是，我们需要在两个文件之间切换。Vim 对最近编辑的文件（上面提到的列表里标有“#”的文件）有特殊的支持，使用快捷键 `&lt;C-^&gt;` 可以在最近的两个缓冲区之间来回切换。这个快捷键还有一个用法是在前面输入缓冲区的编号：比如，用 `1&lt;C-^&gt;` 可以跳转到第一个缓冲区（跟命令行模式的命令 `:bfirst` 或 `:b1` 效果相同）。

从实际使用的角度，使用缓冲区列表有点像打开最近使用的文件菜单（但缓冲区列表不会存盘），可以当作一种快速切换到最近使用的文件的方式。

缓冲区是文件在某个 Vim 会话里的映射。这意味着，如果某个 Vim 会话里不同的窗口或标签页（下一讲里会讨论）编辑的是同一个文件，它们对应到的也会是同一个缓冲区。更重要的是，文件/缓冲区的修改在同一个 Vim 会话里是完全同步的——这就不会像在多会话编辑时那样发生冲突和产生错误了。

## 内容小结

本讲通过讨论使用 Vim 在多个文件里粘贴代码的多种方法，我们学习了以下知识：

- 在图形界面和终端里，粘贴系统剪贴板的内容需要使用不同的方法：前者使用 Vim 命令，后者则需进入插入模式，使用终端的粘贴命令进行粘贴
- Vim 能在崩溃后恢复未存盘的内容，也能在多会话编辑同一个文件时检测到这种冲突
- 在 Vim 里我们可以使用通配符“*.后缀”和“**/*.后缀”来打开多个文件
- 使用 `:args` 命令我们可以展示或替换参数列表，使用 `:next` 等命令我们可以在这些参数指定的文件中切换
- 使用 `:buffers` 或 `:ls` 命令我们可以展示缓冲区列表，即所有已编辑和将编辑的文件，使用 `:b` 和 `:bnext` 等命令我们可以在这些缓冲区中进行切换

今天讲到了一些命令行模式的命令，你应该可以看到，它们都是非常有规律的，最基本的操作就是“first”、“last”、“next”、“Next” 或 “previous”等英文单词，以及它们与前缀的组合。把命令行模式的命令记住，就能完成基本的编辑任务；至于像 `&lt;C-^&gt;` 这样的正常模式命令，万一记不住，也可以用命令行模式的命令来替代。但是，正常模式的命令更加高效，有助于提高你的编辑效率，所以最好通过多加练习来形成“肌肉记忆”。

对于配置文件，本讲只有很小的更改，对应的标签是 `l5-unix` 和 `l5-windows`。

## 课后练习

请在课后进行以下练习，熟悉今天所讲的内容：

1. 用 Vim 打开一个文件，进行编辑（不存盘），然后将这个 Vim 进程 kill 掉；重新打开文件，恢复其中内容并存盘；再次打开文件，删除交换文件。
1. 用 Vim 打开一个文件，然后在另外一个终端窗口里再次打开这个文件，阅读冲突信息，然后退出编辑。
1. 使用 Vim 打开多个文件，逐个查看，然后退出。

我是吴咏炜，我们下一讲再见！
