143-Vim 实用技巧必知必会

导读｜池建强：Vim 就是四个字“唯快不破”

你好，我是池建强。

操作系统、编程语言和编辑器是程序员永恒的讨论话题，技术发展了几十年，大家聊起这个来依然能争个面红耳赤。今天我就和你聊聊文本编辑器的那些事儿。

在文本编辑器领域，Vim 和 Emacs 是永恒的焦点。使用 Emacs 和 Vim 的程序员，平时大家各用各的，各自沿着不同的道路和目标前进，但总会在某个场景下相遇，愣一下就互相扔石头和臭鸡蛋，砸得对方鼻青脸肿，然后擦擦眼泪和口水继续前行。你看，编程也是有宗教信仰的，其实是个危险的工种，当真不是瞎扯。

我工作了二十多年，写程序有十几年，用过各种编程工具，用错过，也用对过，虽然每种优秀的编辑器都有传奇的故事，每个程序员都有自己的脾气，但是，如果让我推荐一款编程工具，那一定是 Vim。

Vim 号称编辑器之神，唯快不破，可扩展，插件遍天下。学习曲线虽然陡峭，但是学成之后，基本上就成肌肉记忆了，写程序双手不离键盘，上下翻飞，可谓快意编程。

我和 Vim 怎么结缘的呢？那得从 2000 年说起。

缘起

当时正值第一波互联网浪潮，我刚毕业不久，一如现在的热血青年，投身到互联网的大熔炉中。我所在的公司叫洪恩教育，公司里聚集了很多清华北大的同学，技术牛人扎堆，大家清一色使用 Vim 在服务器端编程，语法高亮都不设，内部 BBS 也是水木清华那种，通过终端访问，非常极客。走进办公室一眼望去，满目皆是黑漆漆的屏，绿瓦瓦的字，每个人都在那里噼噼啪啪地敲击键盘，韵律十足，我想，这简直酷毙了。

水木清华 BBS

我最初还在使用 Editplus 编程，隶属菜鸟帮。别人的开发、编译和发布环境都在服务器端，而我则需要在本地编写好程序，通过 Editplus 的 ftp 功能上传到服务器端，再进行调试、测试和发布，不仅麻烦，而且不够酷。

那时候不酷是不行的，我这种行为遭到了小伙伴的无情嘲讽，于是我把愤怒都发泄在键盘上，每天在满天星斗的夜色中学习 Vim 技法，在清晨的微光中编译 Linux 内核，上午敲打键盘输出 Perl 程序，中午吃完五又四分之一口米饭之后开始研习 Vim 的多窗口和标签……

那时候我住在公司，时间充裕到让你不好意思不学习，虽然 Vim 资料匮乏，但我周边都是牛人啊，随时随地请教，不断练习，很快小有所成，编码时鼠标锁进抽屉，双手敲击键盘上下翻飞，成就感十足。我对语法高亮情有独钟，经常把自己的界面配置得花花绿绿，没事看看也是一件乐事。

自此以后，我就与 Vim 结下不解之缘，十几年过去了，工作中一直没有离开过 Vim，断断续续一直在用。到了 2009 年，我开始把工作环境完全切换到了 Mac 上，记得当时打开 Mac 的终端时，欣喜若狂地想，这不就是 Vim、Shell 和 IDE 的完美集成么？

场景

在不同的场景下应该采用最适合的工具，这时就会有人问了，Vim 适合什么场景呢？

简单说来，Vim 比较适合 Unix/Linux 服务器端编程，如果你使用 Mac 电脑，Vim 是直接集成在你的终端环境中的，用起来十分方便。我以前用 Vim 主要用来进行 Shell/Python/C 编程。在 Unix/Linux 服务器端编辑和修改文件也离不开 Vim，另外由于我个人工作环境是 Mac，所以修改文本文件、Code Reiview、批量替换文件、比对文件等工作，用 Vim 顺手就做了。

写 Java 程序、前端 HTML/CSS/JS、Objective-C 和 Swift，最优方案依次是 IDEA、VS Code、XCode 等，这些优秀的工具可以帮助我们提升效率，减少错误，但是如果你还想更进一步，那么 Vim 绝对值得拥有。

历史

Vim 源于 vi，但不是 vi，vi 作为计算机的文本编辑器历史极为悠远，它是由美国计算机科学家比尔·乔伊编写并于 1976 年发布的，同年苹果公司成立。比尔·乔伊是 Sun 公司的联合创始人和首席科学家，一位传奇的技术天才，我个人以为他最伟大的贡献是独立编写 BSD 操作系统，开发 vi 编辑器，创立 Sun 公司，当然，他还是 Java 语言的主要贡献者之一，任何人有幸完成其中一项工作已经足以名垂计算机发展史，而乔伊则通过一己之力完成了这些科技成果，推动了整个计算机科技的发展。

Vim 诞生得要晚一些，它的第一个版本由布莱姆·米勒在 1991 年发布，这个兄弟也是一位声名显赫的程序员，80 年代买了一台 Amiga 电脑，打开电脑一看，米勒鼻子差点气歪了，居然没有他最常用的 vi 编辑器！对于米勒来说这是不可接受的。

愤怒的米勒决定自己开发一个文本编辑器，完全复制 vi 的功能，并起名为 Vi IMitation（模拟）。事实证明，优秀的程序员都具备这种品质，感到不爽了，就会写出个什么东西，要么完善一下，要么创新一下，要么是你写，要么是我写，于是很多伟大的软件程序就发明出来了。随着 Vim 的不断发展，更多更好的功能被加了进来，正式名称改成了 Vi IMproved（增强），也就形成了现代的 Vim，目前最新的稳定版本是 8.2，Vim 的开发语言是 C 和 VimScript。

理念

Vim 是一款完全面向程序员的软件，我很少见到用 Vim 编辑文字的普通用户，如果你是，一定要告诉我。

写过程序的人都知道，编程的时候双手大部分时间都放在键盘上，或编码、或插入、或移动、或定位、或查找，这种连续操作的时间和频率远远大于阅读、翻页、设置字体、摆弄样式等文案工作，而二者往往产生很多停顿和间隙，而编程时的停顿是非常影响编程效率的，所以 Vim 的设计理念就是通过模式的转换、命令的组合和数以万计的插件，保证程序员在编程的过程中，双手尽可能保留在键盘中央的区域，并且，不需要鼠标。

想用好 Vim，先要理解 Vim 的模式转换。Vim 常用的模式有四种：

普通模式：Vim 启动后的默认模式，用来移动光标、删除文本、覆盖输入文本、恢复操作、粘贴文本等等。

插入模式：输入 i 或 a 进入插入模式，在这个模式下敲击键盘会往文字缓冲区增加文字，相当于普通编辑器的编辑模式。

可视模式：选择文本，可以行选、块选和依次选择，选择后可以进行复制、删除、排序等操作。

命令模式：执行内部和外部命令，通过“:”“/”“?”“:!”可以进入命令模式，分别对应的是：执行内部命令、向上或向下搜索、执行外部命令。

Vim 的模式和普通的编辑器有所不同，而且命令繁多千变万化，所以初期的学习曲线较高，一旦你坚持练习并且度过了最早的平台期，就会领略 Vim 的妙和全键盘的好。事实上 Vim 除了能够快速编辑文本文件之外，还能够通过简单的命令做更多的事情。

如何帮助你度过这个陡峭的学习曲线呢？这就是极客时间开设 Vim 专栏的初衷，吴咏炜老师是前英特尔亚太研发中心资深系统架构师，是《现代 C++ 实战 30 讲》的专栏作者，也是一位深度 Vim 用户。

他会带着你掌握即学即用的 Vim 高频命令，了解 Vim 高阶用法和技巧，利用 Vim 脚本和插件实现常用功能，以及定制自己专属的 Vim 环境。

一旦你跟着吴老师学完了专栏，真正掌握了 Vim 这个高效率的工具，你会进入一个全新的世界，以前繁琐的编程和文本处理会变得简单有趣，如果你有兴趣，完全可以根据专栏所学的内容，定制一个强大的个性化开发工具，提升自己编程和日常工作效率。

下面我就交棒给吴咏炜老师，让我们一起开始学习吧。

开篇词｜我们为什么要学 Vim？

你好，我是吴咏炜。

今年 2 月，我在极客时间的第一门课程《现代 C++ 实战 30 讲》结课了。现在，我给你带来了一个全新的课程，《Vim 实用技巧必知必会》。

这两门课虽然主题不同，但我准备它们的初衷却是完全一样的。我在 C++ 课程里分享过，我特别喜欢 Larry Wall（拉里 · 沃尔）所说的程序员的三大美德：懒惰，急切，傲慢（laziness, impatience, hubris）。正是这些美德，驱动我们不断地追求效率和极致，而 Vim 就恰恰是这样一个效率利器。我想通过接下来的课程，和你一起享受 Vim 带来的技术乐趣。

Vim 的“前世今生”

说到 Vim，就不能不谈一下 vi；要说 vi，那跟 Unix 就有着千丝万缕的联系。万物起源总有那么点故事，我们这个课程的主题也不例外。这个故事可以讲上很长时间，不过，今天不是故事会时间，我只会花几分钟给你快速梳理一下这段历史，带你了解 Vim 的“前世”，也就能更好地理解它的“今生”。

故事的开头是在 1975 年秋天，Unix 诞生之后的第六年。Ken Thompson（肯 · 汤普逊）来到了加利福尼亚大学伯克利分校，开始了为期一年的访问教授生活。当然，他也带上了最新版本的 Unix 的补丁。根据传说，由于贝尔实验室律师们的阻挠，他不能直接把补丁给其他人，而只能把装有补丁的磁带“丢”在某个地方，然后由别人“正好”捡到……Unix 就以这种“地下”的传播方式流传开了。

同年，年轻的 Bill Joy（比尔 · 乔伊）也进入了加利福尼亚大学伯克利分校，学习电子工程和计算机科学。他立刻就迷上了 Unix。在后面几年的硕士生涯里，他修正了 Unix 里的 Pascal 系统，使得 Pascal 成了学生编程的缺省选择。他在 1978 年负责发布了第一个伯克利发行版（BSD，即 Berkeley Software Distribution），其中包含了他写的 ex，一个编辑器 ed 的改进版本。随即，在 1979 年他发布了第二版的伯克利 Unix（2BSD），包含了他写的 vi 和 csh。他独立实现了 BSD 中的 TCP/IP 栈。

离开伯克利后，他成了 Sun 的联合创始人和首席科学家，在 Solaris 操作系统、NFS 网络文件系统、SPARC 处理器、Java 语言的开发等多个领域中作出了自己的贡献……

在 Bill Joy 的无数传奇故事里，有一个是，他只花了一个周末就写出了 vi。这当然……不是真的。vi 是演进的结果，前面还有 ed、em、en、ex（哈，对于两字母的 Unix 命令，我看得也是有点晕了🤔）等等一系列。只不过，那些都是基于命令的行编辑器，而不是全屏编辑器（部分原因是那时的很多系统仍然使用着电传打字机，而不是 CRT 终端）。vi 可以充分使用整个终端屏幕的资源，易用性的提升是毋庸置疑的。

不管怎么说，vi 只是一个 Bill 无意插柳柳成荫的结果，是他职业生涯中的一个副产品而已。在 1982 年初，Bill Joy 加入 Sun 公司之后，vi 就不怎么有人维护了。此外，由于没有得到 AT&T 授权的公司和个人也不能使用 vi 的源码（律师又一次发挥了威力），因此，大量的 vi 克隆版本纷纷出现。

目前大部分 Linux 发行版和 macOS 中的 vi 命令唤起的都是 Vim，一个由 Bram Moolenaar（布莱姆 · 穆勒纳尔）持续开发维护了三十多年的 vi 克隆（想想，三十年在计算机的发展中，那是经历了多少代技术的演进！）。在这些年里，其他的 vi 克隆诞生又死去，最后只剩下了 Vim（好吧，“只”是夸张手法）。起初，Vim 的意思是 Vi IMitation，但很快就成了 Vi IMproved。而这，就是我们这个课程的主题。

三位大牛

Vim 的优势

vi 有着一个非常老古董的设计，就是它是一个有“模式”的编辑器。其他大部分编辑器都相当于 vi 的插入模式，输入什么字符就会在屏幕上出现什么字符。但 vi 的行为不是如此。

事实上，这种不那么直观的设计，即使在 vi 初次出现的 20 世纪 70 年代，也被认为是违反人机交互的原则的。所有的后续 vi 实现，包括 Vim，都继承了 vi 的模式设计。

令人惊讶的恐怕是，尽管有这些问题，Vim 在程序员群体中的流行程度并没有受到影响。根据 Stack Overflow 的开发者调查：2015 年程序员中最流行的编辑器是 Notepad++，Vim 的使用比例是 15.2%；2019 年最流行的开发环境变成了 Visual Studio Code，而 Vim 的使用比例还保持在了 25.4%。

这就是这门课程从头到尾都会试图回答的问题：Vim 到底好在哪里？

拿我自己来说，我刚开始使用 Vim 时，不是出于选择，而是在 Linux 上开发的需要。不过，用着用着我就喜欢上 Vim 了——不仅在 Linux 下用，也在 Windows 下用（从十几年前开始，我就一直自己编译和维护着一个 Windows 下的最新 Vim 可执行文件）。自打切换到了 Mac 上之后，当然就更不用说，MacVim 是日常打开次数最多的工具。这里面最最主要的原因，就是使用 Vim 编辑文件非常高效。

在很大程度上，vi 的“高效”是一种历史性的设计要求，当年程序员需要在网速 300 波特（大致认为是今天网速的百万分之一吧）的环境里编辑文本文件。那个时代，人们还不可能拥有自己的计算机，大学、政府、公司里的计算机全都通过终端来进行分时共用。因此，vi 在命令上不得不非常“经济”。好玩的是，这种经济性，在今天仍然非常有用，它是 vi 及其克隆软件的高效之源。

我已经强调了几遍了，编辑的高效性，就是 Vim 最大的一个特点。除此之外，Vim 的优势还有很多，我来给你分享一下我认为最重要的三点。

第一，与 vi 最初只运行在 Unix 平台上不同，Vim 是一个完全跨平台的编辑器。

它支持的第一个操作系统是 AmigaOS，然后被逐步移植到了大部分其他操作系统上，既有我们常见的 Unix/Linux、Windows、macOS，也有不常见或者过时的操作系统，如 OS/2、BeOS、OpenVMS，甚至在 iOS 和 Android 上也能找到 Vim 的移植版本。这可以算是 Vim 的一个重要优点了。这个课程里，我会介绍 Vim 在主流操作系统上的使用，包括 Linux、macOS 和 Windows。

第二，Vim 也是一个高度可定制、可扩展的编辑器。

这对热爱折腾的程序员来说，绝对是一种乐趣，同时也是进一步提升效率的源泉。定制 Vim ，大部分情况下，你不需要什么特殊工具，使用 Vim 本身就可以。Vim 有自己的脚本语言，就叫 Vim 脚本（Vim script），语法相当简单，任何一个程序员应该都可以轻松地学会。配置文件和功能扩展都使用 Vim 脚本，使用统一的语法。同时，需要更强大的扩展能力还可以使用 Python、Perl、Ruby、Tcl 等其他通用的脚本语言，或者直接调用外部命令。你可以很容易打造一个你自己专属的开发环境，也很容易把这个环境从一台机器转移到另外一台机器上。

第三，作为一个发展了几十年的老牌开源软件，Vim 也有着良好的生态环境。

网上可以找到大量的现成脚本和插件，能帮助你打造一个顺手的开发环境。总的来说，像语法检查、自动补全等程序员常用功能，全都可以在 Vim 里实现。你不需要离开 Vim，就可以完成从写代码、编译到运行的大部分工作。下面的这张图里，就展示了 Vim 的很多扩展一起工作的结果：

多个插件作用下的 Vim 界面

你可以看到，左边栏展示了 Vim 相对当前 Git 版本的修改状态（一处增，一处改），波浪线标出了代码中目前有错误的部分，底部显示了错误的原因，下面有个小窗口显示了光标所在处相对 Git 版本的变化，状态栏里更是密密麻麻地显示了编辑器模式、Git 分支、文件名、修改状态等信息。这里面用到了好几个扩展，包括颜色主题也是一个扩展。

虽然 Vim 最初是个针对字符界面的应用程序，但它也能支持主流的图形界面，包括 Windows 的图形界面，Linux 下的 GTK，以及 macOS 下的 Cocoa 和 Carbon，等等。作为一个并非“原教旨主义”的 Vim 用户，我个人是绝对赞成图形界面的使用的。因而我会推荐，只要有条件，就使用有图形界面的 Vim 版本。

不过，这个课程的绝大部分内容是对图形界面和文本界面都有效的（在两者有区别的地方，我则会明确指出）。换句话说，在你只能使用基于字符界面进行远程连接时，Vim 的功能仍然大部分有效，只是界面的美观程度会受一定的影响而已。也由于这个原因，Vim 在后端开发人员中特别受欢迎。

Vim 的模式是 Vim 的高效所在，但同时也是 Vim 学习上的一个难点。略有点搞笑的是，Stack Overflow 上有一个目前票数达到 3840 的问题是“如何退出 Vim 编辑器”，按问题票数排名可以进入前 100，这可能就是 Vim 的模式造成的困惑了。

反过来，这个反常规的设计使得 Vim 可以使用很逻辑的多个按键来处理文本，比如，在正常模式使用 daw 三个按键代表 delete a word 来删除光标下的一个完整单词，也可以输入 : 进入命令模式使用“make”这样的完整命令来进行项目的构建。

整体来说，Vim 会给你一个高效、跨平台、高定制性、易于扩展的开发环境。全面掌握 Vim 需要花费一定的时间进行学习。但这个时间不会白费，因为 Vim 可以在任何地方使用，它会成为你编程道路上一件称手的兵刃，让你成为更加高效的开发者。

课程主要内容

比起很多“开箱即用”的编辑器，Vim 是有一定的学习曲线的。虽然学 Vim 比学编程容易多了，但对于非英语母语的人来说，Vim 又会难上一点点。我会尽量多讲原理，而不是枯燥地讲解命令。不得不讲命令的时候，我会使用图片和动画，让你能对相关内容有一个直观的理解。对于很多 Vim 的命令，我们是需要形成“肌肉记忆”的；我们不需要死记硬背，但需要多看、多练，熟能生巧，在学习过程中自然而然就掌握了使用 Vim 的技巧。

作为一个有历史的编辑器，Vim 一直保持着非常良好的向后兼容性。学 Vim 学到的东西不会过时，在你的程序员生涯中一直可以用下去。我个人的 Vim 配置文件始于约 20 年前，慢慢地添砖加瓦，一直用到了今天。同时，Vim 也一直在发展，虽然不快，却也从来没有停下来（从发布 8.0 版本算起，平均每天 3.7 个补丁吧）。

在这个课程中，我会基于目前最新的 Vim 8.2 来讲解 Vim 的功能。你将会学到：

Vim 的安装

Vim 的模式和命令

Vim 的配置

Vim 的使用技巧

Vim 里的重要插件

及最最重要的

如何把 Vim 集成到你的工作流里，让它成为一件称手的工具，来进行高效的编辑

课程学习要求

我对这门课程的定位是零基础课，哪怕你以前没有用过 Vim ，也完全可以上手。这个课程不是 Vim 的百科全书，不会把 Vim 的所有命令选项，不管有用没用，全部都教给你。这是一个“新”教程，里面讲述的版本、很多技巧和插件是最近几年才有的，甚至是我在写专栏的时候才发现的。这也是一个面向实践者的教程，会让一个需要或想要使用 Vim 的开发者，从入门到精通，学会高效地使用 Vim 完成程序或其他文本文件的编辑。

虽然课程定位是零基础，但这并不意味着我对你没有任何要求。我仍然要求你在学习课程前：

熟悉你使用的平台上的包管理器（yum、apt、brew 等；仅类 Unix 环境），知道如何完成程序的安装和卸载；

安装了 Git，并对 Git 操作有基本的概念（不要求熟练掌握，因为我会给出大部分情况下需要的命令）；

有一颗勇于探索的心，愿意花点力气把手里的“武器”打造得更为好用、称心。

使用 Vim 有不同的场景。在我设想的环境里，你是一个程序员，但我不对语言作出要求。课程的大部分内容完全是语言无关的，无论你使用什么编程语言开发，都应该可以获得有用的知识。

不过，如果你使用 C、C++ 或 Python 进行开发，你可以得到一些额外的福利，因为这些是我主要使用的语言。这三个语言的额外重要性在于，Vim 的插件有可能会用到这三种语言。

如果你使用其他语言的话，就多多留言，在留言区介绍你自己使用 Vim 的情况，有啥问题和困惑一定要及时提出来，这样，我也可以更有针对性地解决你的问题；如果说得太晚了，我可能就帮不上忙了哦😁。

好了，闲话就说到这里。下一次，我们就进入正题，从 Vim 的基本安装和配置开始讲起。

我是吴咏炜，我们下一讲见。

01｜各平台下的 Vim 安装方法：上路前准备好你的宝马

你好，我是吴咏炜。

今天第一讲，我们先来讨论一下 Vim 在 Linux、macOS、Windows 系统下的安装和配置问题。

Vim 在 Linux 和 macOS 上一般是默认安装的，在 Windows 上不是。不过 Vim 的网站上提供了 Windows 下的安装包，自己安装也很容易。所以，今天的课程我不会手把手、一步步地讲，而是挑选一些重点。对于默认安装的情况，主要讨论的是版本老旧或功能不全的问题。对于其他情况，我则会给出一个基本指引，减少你走弯路的可能性。

好了，下面我们就分各个不同的平台，一一来看。

Linux 下的安装

Red Hat 和 CentOS 系列

在 Red Hat Linux 和 CentOS Linux 上，默认安装的 Vim 可能是一个最小功能的版本。虽然这个版本启动速度很快，但它缺少了很多对开发有用的功能，如语法加亮、Python 集成和图形界面。一般情况下，应至少安装更全功能版本的 Vim；如果能使用 X Window 的话，则应安装图形界面版本。

你可以通过下面的命令来查看已经安装的 Vim 版本：

yum list installed | grep vim

如果输出只有下面这样的内容的话，就说明安装的 Vim 版本只有基本功能：

vim-minimal.x86_64 2:8.0.1763-13.el8 @System

此时，我建议使用 sudo yum install vim-X11 来安装图形界面的 Vim，或至少使用 sudo yum install vim-enhanced 来安装增强版本的 Vim（如果你不在这台机器上进行图形界面登录的话）。

只要你使用图形界面，一般而言，你都应该安装有图形界面支持的 Vim。总体而言，图形界面 Vim 的功能更丰富，并且即使你只在终端里使用 Vim，含图形界面支持的 Vim 会带剪贴板支持，跟整个图形环境的交互也就比较容易。当然，如果你只是远程通过 SSH 使用 Vim 的话，那确实图形界面支持就没有意义了。

Debian 和 Ubuntu 系列

在 Debian、Ubuntu 等使用 apt 的 Linux 发行版上，Vim 同样有着不同功能版本的区别，而且选择更多。我们可能会看到：

vim

vim-athena

vim-gnome

vim-gtk

vim-gtk3

vim-nox

vim-tiny

它们中有编译进最小功能的 Vim 包（vim-tiny），有较全功能的文本界面 Vim 包（vim-nox），有适用于老的 X-Window 界面的版本（vim-athena），有适用于 KDE 环境的 GTK2 版本（vim-gtk），等等。

一个估计你不会去用的古老界面的 Vim（Athena 界面）

如果你不确定要装什么版本的话，那可以遵循我下面的建议：

如果你使用标准的 GNOME 桌面环境的话（大部分的情况），安装 vim-gtk3 或 vim-gnome。

如果你使用 KDE 桌面的话，安装 vim-gtk。

如果你只使用文本界面的话，安装 vim-nox。

都不是？那你是个爱自己定制的家伙，也就不需要我告诉你该安装什么了。

你可以通过下面的命令来查看已经安装的 Vim 版本：

apt list --installed | grep vim

我们先执行 sudo apt update 来确保更新环境，然后使用 sudo apt install vim-gtk3 安装 GTK3 版本的 Vim（或者其他你需要的版本）。如果你安装了图形界面的版本，不必单独再另外安装其他版本的 Vim，因为图形版本的 Vim 也是可以纯文本启动的。事实上，在 Ubuntu 上，vim 和 gvim 都是指向同一个应用程序的符号链接，且 gvim 的执行效果和 vim -g 相同。

在终端中运行 vim.gtk3，执行 :version

手工编译

如果你用的 Linux 发行版较老的话，你可能会希望手工编译 Vim 来得到最新的版本。此时需要注意的是，Vim 有很多的编译配置选项，有些缺省是不打开的。对于这个课程来讲，我们会希望至少加上 Python 支持和图形界面支持。

你首先需要确保自己已经安装了所需的开发包（可以参考这个网上的回答）。然后，我们可以使用下面的命令来配置 Vim 和编译（根据需要，“auto”也可以替换成“gtk3”等其他需要的数值）：

./configure --enable-pythoninterp \
            --enable-python3interp \
            --enable-gui=auto
make -j
sudo make install

如果上述步骤正常没有出错，Vim 就被成功安装到 /usr/local 下了。你可以用 which vim 来检查系统是否会自动优先选择你的 vim：如果不是的话，你可能需要调整 PATH 里的顺序，或者设置别名来优先启动 /usr/local/bin/vim）。然后，你可以使用 vim --version 命令来输出 vim 的版本信息。我们希望能在输出里看到：

Huge version with … GUI

+python/dyn

+python3/dyn

目前 Python 2 已经停止支持，所以你最好可以确保上面的输出中包含“+python3”（很多 Vim 的插件已经开始要求 Python 3、不再支持 Python 2 了）；没有“+python”（即 Python 2）则没什么关系（有没有“dyn”关系也不大）。

我在 CentOS 7 上编译的 gvim

好了，关于 Linux 环境下的 Vim 安装和配置要点我们就讲完了，接下来继续看在 macOS 上如何安装。

macOS 下的安装

在 macOS 中一般已经内置了 vim，并提供了除图形界面外的较完整功能集。如果你希望使用图形界面，则需要自行安装 MacVim，一个跟现代 macOS 融合较好的独立 Vim 版本。安装 MacVim 有两种常用方式：

使用 Homebrew。我推荐你使用这种方式，这样的话，以后升级也会比较容易。

使用 MacVim 的独立安装包。如果你之前没有在用 Homebrew 的话，或处于不方便使用 Homebrew 的网络环境中，这种方式也可以。

由于使用 Homebrew 已经足够简单，日后升级也非常方便，我个人觉得我们没必要自己去编译 MacVim。

MacVim 的界面

使用 Homebrew 安装 MacVim

首先，如果你没有 Homebrew，那你需要先安装 Homebrew。安装信息可以在 Homebrew 的主页上找到（这个网站是支持中文的）。

在安装了 Homebrew 之后，一般情况下，你需要修改你的 .bash_profile（如果使用 Bash 的话）、.zprofile（如果使用 Zsh 的话）或是相应的 shell 的配置文件，调整 PATH，把 /usr/local/bin 放到 /usr/bin 前面。我个人在 .bash_profile 里是这样配置的：

if [[ $PATH != "$HOME/bin"* ]]; then
  PATH=~/bin:/usr/local/bin:/usr/local/sbin:`echo $PATH|sed -e "s!:$HOME/bin!!" -e 's!:/usr/local/bin!!'`
fi

这样，可以确保个人的路径优先于 /usr/local，而 /usr/local 下的路径又优先于系统的路径。

如果你这样配置的话，那只要执行 brew install macvim，然后在等待安装完成之后，你用 vim 启动的就是 MacVim 了。缺省用 vim 运行的仍然是纯文本界面的 Vim，但跟 Linux 一样，你可以用 vim -g 或 gvim（还有仅用在 Mac 上的 mvim）来启动 Vim 的图形界面。

跟 Homebrew 里的其他软件一样，你以后要升级 MacVim 的话，只需要输入命令 brew upgrade macvim 即可。是不是很简单？这就是为什么我比较推荐这种安装方式，后期升级真的更容易。不过我下面还是会介绍下安装包的方式，以满足我们不同的应用需求。

使用安装包安装 MacVim

跟大部分的 Mac 软件一样，你也可以直接使用 DMG 安装包来安装 MacVim。目前可从以下网址下载 MacVim 的安装包：

https://github.com/macvim-dev/macvim/releases

等待下载完成后，双击下载的文件，然后会打开一个访达（Finder）窗口。你只需要把 MacVim 拖拽复制到应用程序文件夹即可。

在这种安装方式下，手工键入 vim、gvim 或 mvim 命令是无法启动 MacVim 的。你需要手工创建这些命令的符号链接（symlink）或别名（alias）才行。假设你的 MacVim 是直接安装在应用程序文件夹里的话，这些命令本身可以在 /Applications/MacVim.app/Contents/bin 文件夹里找到；使用下面的命令可以在你自己的 bin 目录下创建这些命令的符号链接：

[ -d ~/bin ] || mkdir ~/bin
ln -s /Applications/MacVim.app/Contents/bin/* ~/bin/

Windows 下的安装

最后，我们来看在 Windows 下怎么安装。课程开头我提到了，Windows 上缺省是没有 Vim 的。我们可以从 Vim 的网站下载 Windows 下的安装包：

https://www.vim.org/download.php#pc

在 Linux 和 macOS 上，64 位应用程序已经成为主流。而与此不同的是，在 64 位 Windows 上，32 位应用程序仍然很常见。默认的 Vim 8 的安装包安装的仍然是一个 32 位的应用程序。不过，32 位的 Vim 也足够满足一般需求了，除非你需要编辑 2 GB 以上的大文件。

安装界面会有一个选择组件的步骤，如下图所示：

Vim 的组件选择界面

这个界面中，下面几项我们可以关注一下：

“安装批处理文件”（Create .bat files）：对于用 Vim 的开发者来说，通常命令行是刚需，所以我们一般需要勾上这项。

“创建图标”（Create icons for Vim）：根据你自己的需要进行选择，通常我会去掉展开子项里的“桌面图标”（On the Desktop），不在桌面上创建 Vim 的图标。

“创建默认配置文件”（Create Default Config）：去掉这项——我们马上会创建配置文件。

“安装多语言支持”（Native Language Support）：这项功能使得 Vim 的菜单可以显示中文的命令，但实际上还是有点鸡肋，因为 Vim 的主要功能不是靠菜单驱动的，安装程序安装的帮助文件也只有英文版。所以，这项选和不选关系不大，你可以自由选择。

然后我们点“下一步”（Next），不需要修改安装目标文件夹，完成安装即可。

完成安装后，Vim 会缺省打开一个 README 文件。在这个窗口中，我们应当键入“:e ~\_vimrc”，回车键，然后把下面的内容粘贴进去（这些配置项的意义我们以后会讨论）：

set enc=utf-8
set nocompatible
source $VIMRUNTIME/vimrc_example.vim

然后键入“ZZ”（大写）存盘退出即可。

创建基本的 _vimrc 配置文件

注意由于历史上的文件系统限制，在 Windows 下 Vim 的配置文件名称是 _vimrc 而不是 .vimrc（虽然 Windows 命令行不支持像 Unix 一样用“~”代表用户的主目录，在 Vim 里我们仍然可以使用“~\_vimrc”或“~/_vimrc”这样的写法）。这是 Unix 和 Windows 下的 Vim 配置的区别之一。其他的主要区别是以下两点：

点打头的 Vim 文件都成了“_”打头，如 .viminfo 也成了 _viminfo

点打头的 Vim 配置目录 .vim 在 Windows 下则成了 vimfiles

除此之外，Vim 的配置在 Windows 下和 Unix 下（如 Linux 和 macOS）并没有根本不同。Windows 上的主要麻烦在于，由于 Vim 的生态主要在 Unix 上，某些 Vim 的插件在 Windows 上安装配置需要花费更大的力气。但就一般的文本和程序编辑而言，Vim 在 Windows 下和 Linux 下没有本质的不同。甚至 Windows 下还有一个小小的优势：Unix 下虽然 Vim 可以编译成支持 Python 2 和 Python 3，但在 Vim 里一旦执行了 Python 2 的代码，就不能再执行 Python 3 的代码了；反之亦然。Windows 下则没有这个限制。

有没有注意到我只在 Windows 的安装部分讨论了配置？这是因为 Unix 下主流的缺省编码已经是 UTF-8 了，而 Vim 只能在内码是 UTF-8 的情况下才能处理多语言的文本。而我们有自己的配置文件，是为了确保启用一些最为基本的配置选项，来保证基本行为的一致性。

Windows 上可以把 Vim 配置成跟普通的编辑器行为差不多，包括支持 Ctrl-A 全选，选择内容后输入任何内容替换选择的内容，等等。但是，这种行为跟 Vim 的标准行为是冲突的。我们要学习 Vim，还是忘了这些 Windows 特有的功能为好，去学习掌握 Vim 的跨平台标准功能。上面的配置文件也同样没有启用 Windows 下的特有行为。

Cygwin/MSYS2

Windows 有 Cygwin 和 MSYS2，可以提供类似于 Linux 的 POSIX shell。在这些环境里，Vim 都是标准组件，按这些环境的标准方式来安装 Vim 就行。如果你使用 Git Bash 的话，里面就直接包含了 MSYS2 的终端、Bash 和 Vim。唯一需要提一句的是，这些类 POSIX 环境里面的 Vim 配置应当参照 Linux 终端来，而不是 Windows 下的标准方式（也就是说，个人配置目录和配置文件是 .vim 和 .vimrc，而非 vimfiles 和 _vimrc）。我以后对这种情况就不再单独描述了。

远程使用 Vim

还有一种常用的环境恐怕是使用 mintty、PuTTY、SecureCRT 之类的软件在 Windows 上远程连接到 Linux 机器上。在这种情况下，需要特别注意的，是远程终端软件的远程字符集（如 PuTTY 中的“Windows > Translation > Remote character set”）应当设置成 UTF-8。这个设定跟具体的软件及其版本有关，我就不详细说明了；请自行查看你所使用的远程终端软件的设定和相关文档。

学习 Vim

上面我们讲解了 Vim 的安装。如果安装过程中遇到了什么问题，可以留言提问。接下来，我会给你提供一些 Vim 的学习资料，帮助你进入 Vim 的世界。你应该仔细看一下你所使用的平台上的 Vim 安装信息（其他平台的可以略过），并且应该自己打开 Vim 教程练习一遍（除非这些基础知识你都了解了）。键盘配置相关信息属于可选，可以根据自己的兴趣和需要决定是否了解一下。

中文帮助文件

Vim 内置了完整的英文帮助文件。如果你想要中文帮助文件的话，有个好消息是，有网友同步翻译了最新的帮助文件，而且安装过程在 Vim 8 （或将来的版本）里是非常简单的。以 Unix 下为例（Windows 下类似，但路径 .vim 需要修改为 vimfiles）：

cd ~/.vim
mkdir -p pack/my/start
git clone https://github.com/yianwillis/vimcdoc.git pack/my/start/vimcdoc

如果你不需要以后利用 Git 来快速升级文档的话， 也可以在这个 Vim 中文文档计划的下载页面下载 tar 包，然后自行解压到 ~/.vim/pack/my/start 目录下（或 Windows 用户目录下的 vimfiles\pack\my\start 目录下）。

Windows 用户有一个简单的安装程序（当前为 vimcdoc-2.3.0-setup-unicode.exe），可以自动帮你完成中文帮助文件的安装任务。如果你的机器上没有 git 和 tar 可执行程序的话，那这个方式最简单。

中文 Vim 帮助

Vim 教程

Vim 在安装中自带了一个教程，可供快速入手使用。如果你对 Vim 的基本操作不熟的话，建议你完整学习一下，我也就不必多费笔墨介绍一些最基础的用法了。

Vim 教程支持多语言，可使用命令 vimtutor 来启动。如果启动的教程的语言不是你希望的，你可以使用环境变量 LANG 来设定希望的语言。比如，下面的命令可以在 Unix 环境中启动一个中文的 Vim 教程：

LANG=zh_CN.UTF-8 vimtutor

Windows 下你可以在开始菜单里找到 Vim tutor。但我测试下来它有一个问题。虽然我提交的解决方法已经作为补丁（8.2.0412）合并，但目前（Vim 8.2）安装程序安装的文件多半仍然是有问题的，你会无法成功地创建一个 tutor 文件的副本供编辑使用。我建议手工创建一个这个教程的副本。可以在命令提示符下输入：

vim --clean -c "e $VIMRUNTIME/tutor/tutor.zh_cn.utf-8" -c "w! TUTORCOPY" -c "q"

这样即可在当前目录下创建一个教程的副本。然后我们可以用 gvim TUTORCOPY 来打开这个副本进行学习。

Vim 教程

键盘重配置

最后，有些重度的 Vim 用户会重新配置键盘，把使用频度较低的大写锁定键（Caps Lock）重新映射成 Esc 或 Ctrl 键。对于这个问题，如果你需要的话，网上很容易就能找到攻略，如：

Linux 下将大写锁定键 (CapsLock) 键映射为 Ctrl 键 (Ubuntu, Manjaro，CentOS)

mac book 更改 caps lock 键为 esc 键 /ctrl 键

windows 交换大写锁定键与 ESC 键（注册表修改）

在任何操作系统上，如何禁用或者重新分配 Caps Lock 键

这当然是一件非常个人化的事情，而且有一个风险，你一旦跑到别人的机器上操作，你的“肌肉记忆”可能会让你常常按错键。鉴于你目前可能只是个 Vim 的初学者，现在不一定需要这么去做。等到你觉得按 Esc 太麻烦了，再想起这个可能性去修改键盘配置也来得及。

内容小结

今天我们讨论了 Vim 在常见平台上的安装过程。顺便说一句，以后在牵涉到环境问题时，我一般也会以上面提到的几种典型情况为例来进行讲解：

Linux（CentOS 和 Ubuntu）

macOS

Windows

你可能看着多个平台的安装过程有点晕，这却是我的实际使用环境了——我就是在各个平台下都安装、配置、使用着 Vim 的，这也就是 Vim 的全平台、跨平台优势了。

当然，必须得承认，Vim 还是最适合类 Unix 环境，它的生态系统也是在类 Unix 环境下最好。鉴于在 Windows 下已经越来越容易接触到类 Unix 环境（像 Git Bash、Docker 和 Windows Subsystem for Linux），服务器开发上 Linux 也已经成了主流，在 Windows 上熟悉 Vim 的完整环境对你也应该是件好事——尤其如果你是做服务器或嵌入式开发的话。

下一讲，我们就会进一步学习一下 Vim 的基本概念和配置。

课后练习

如果你之前不常使用 Vim ，请务必花点时间看一下 Vim 教程。在下一讲开始时，我将会假设你已经掌握了 Vim 教程里的基本用法。

当然，如果有任何问题的话，可以在讨论区留言和我进行交流。

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

02｜基本概念和基础命令：应对简单的编辑任务

你好，我是吴咏炜。

这一讲，我们会讨论 Vim 的基本概念和配置。先强调一下，请务必确保你在学习这一讲之前，已经通过 Vim 教程熟悉了 Vim 的基本用法。

Vim 教程的内容概要

上节课我给你留的作业，就是花时间学习一下 Vim 教程，下面我们就来检验一下。只有你自己先对照着教程操作了一遍，今天我再带着你过一遍里面的基本概念和配置，你才能查漏补缺，发现自己遇到的问题，明确自己需要多加练习的地方。

好，现在请查看下面的键盘图。简单说明一下，这张图上展示了一个键盘。图中的“•”表示，单个字母不是完整的命令，必须再有进一步的输入。比如，单个“g”没有意义，而“gg”表示跳转到文件开头。（对于命令后面明确跟一个动作的，如“c”，我们不使用“•”。）一个键最多有三排内容：最底下是直接按键的结果，中间是按下 Shift 的结果（变大写），上面偏右的小字是按下 Ctrl 的结果。我们还用了一些特殊符号来表示操作的位置，如果你已经了解了这些命令的功能，你也自然就明白它们的意义了。

Vim 命令速查（教程版）

请检查一下有颜色的那些键，看看你是否有任何不熟悉的地方。如果看下来有让你感到陌生的内容，请复习 Vim 教程。

这张图里没有写出 Vim 的命令行命令。你现在应该已经掌握了以下这些：

“:q!”：退出 Vim

“:wq”：存盘退出

“:s”：执行替换

“:!”：执行外部命令

“:edit”（一般缩写为 “:e”）：编辑文件

“:w”：写文件

“:r”：读文件

“:help”：查看帮助

使用键 Ctrl-D 和 Tab 来进行命令行补全

同样，如果你发现上面列举的命令有你不熟悉的，也请重新打开 Vim 教程复习一下——这些属于 Vim 的最基本功能，一定要能熟练运用才行。

Vim 的模式

接下来我们进入本讲的正题，讲述 Vim 的四种主要模式、键描述的体例和 Vim 需要的基本配置选项。掌握了这些内容之后，我们就能应对基本的编辑任务了。下面我们一一来看。

Vim 最特别的地方就是它的模式了。与其他大部分编辑器不同，进入 Vim 后，缺省状态下键入的字符并不会插入到所编辑的文件之中。 Vim 的模式（mode，可以简单地理解为“状态”）是它的麻烦所在，但同时也是它的威力所在。

我们需要知道，Vim 有以下四种主要模式:

正常（normal）模式（也称为普通模式），缺省的编辑模式；如果不加特殊说明，一般提到的命令都直接在正常模式下输入；在任何其他模式中，都可以通过键盘上的 Esc 键回到正常模式。

插入（insert）模式，输入文本时使用；比如在正常模式下键入 i（insert）或 a（append）即可进入插入模式。

可视（visual）模式，用于选定文本块；教程中已经提到可以用键 v（小写）来按字符选定，Vim 里也提供其他不同的选定方法，包括按行和按列块。

命令行（command-line）模式，用于执行较长、较复杂的命令；在正常模式下键入冒号（:）即可进入该模式；使用斜杠（/）和问号（?）开始搜索也算作命令行模式。命令行模式下的命令要输入回车键（Enter）才算完成。

此外，Vim 也有个选择（select）模式，与普通的 Windows 编辑器行为较为接近，选择内容后再输入任何内容，将会替换选择的内容。在以可视模式和选择模式之一选定文本块之后，可以使用 Ctrl-G 切换到另一模式。这个模式主要是为了模拟 Windows 编辑器的行为，并不是 Vim 的主要用法，使用它反而会给 Vim 里的自动化带来麻烦，所以我们也就不多作介绍了。

关于 Vim 的模式，我们重点掌握正常模式就可以了，刚刚也说过，Vim 里的大部分操作会在正常模式下完成。如果你做编辑工作时有超过几秒的停顿，就应当考虑按下 Esc 键，回到正常模式。记住，正常模式就是正常情况下你应当处于的模式。😄

Vim 的键描述体例

清楚了 Vim 模式之后，我们来对 Vim 里的按键作一下清晰的体例描述，毕竟，Vim 里的键真的有点多。

从现在开始，我会使用 Vim 里的标准键描述方式来讲解。根据 Vim 的一般习惯，我们使用尖括号来描述特殊的输入序列。下面我会提供一个列表，给出常用键的表示方式及在动图中的显示方式。这部分内容不需要记住，你用的时候作为参考就行。

<Esc> 表示 Esc 键；显示为“⎋”

<CR> 表示回车键；显示为“↩”

<Space> 表示空格键；显示为“␣”

<Tab> 表示 Tab 键；显示为“⇥”

<BS> 表示退格键；显示为“⌫”

<Del> 表示删除键；显示为“⌦”

<lt> 表示 < 键；显示为“<”

<Up> 表示光标上移键；显示为“⇡”

<Down> 表示光标下移键；显示为“⇣”

<Left> 表示光标左移键；显示为“⇠”

<Right> 表示光标右移键；显示为“⇢”

<PageUp> 表示 Page Up 键；显示为“⇞”

<PageDown> 表示 Page Down 键；显示为“⇟”

<Home> 表示 Home 键；显示为“↖”

<End> 表示 End 键；显示为“↘”

<F1> - <F12> 表示功能键 1 到 12；显示为“F1”到“F12”

<S-…> Shift 组合键；显示为“⇧”（较少使用，因为我们需要写 ! 而不是 <S-1>；和特殊键组合时仍然有用）

<C-…> Control 组合键；显示为“⌃”

<M-…> Alt 组合键；显示为“⌥”（对于大部分用户，它的原始键名 Meta 应该只具有历史意义）

<D-…> Command 组合键；显示为“⌘”（Mac 键盘）

现在回到前面的模式部分，我们提到的 Esc、Enter、v、V 和 Ctrl-V，按我们现在的描述惯例，以后就会写成 <Esc>、<CR>、v、V 和 <C-V>。这也是以后在 Vim 里对键进行重映射的写法——如果你还不了解重映射是什么也没关系，我们很快就会讨论到。

这里我要强调一下，对“<”的特殊解释仅在描述输入时生效。在描述命令行和代码时，我们写“<CR>”仍表示四个字符，而非回车键。特别是，如果我们描述的命令行首是“:”，表示这是一个输入 : 开始的 Vim 命令行模式命令（以回车键结束）；如果行首是“/”或“?”，表示这是一个输入 / 或 ? 开始的搜索命令（以回车键结束）；如果行首是“$”，表示这是一个在 shell 命令行上输入的命令（以回车键结束），“$”（和后面的空格）不是命令的一部分，通常后续行也不是命令的一部分，除非行尾有“\”或“^”字符，或行首有“$”字符。

也就是说，下面的命令是在 Vim 里输入“:set ft?<CR>”（用来显示当前编辑文件的文件类型）：

:set ft?

Vim 命令示例

下面的命令则是在 shell 里输入“which vim<CR>”（用来检查 vim 命令的位置）：

$ which vim
/usr/bin/vim

Shell 命令示例

此外，当我用“:help”描述帮助命令时，你不仅可以在 Vim 里输入这个命令来得到帮助，也可以点击这个帮助的链接，直接在线查看相应的中文帮助页面。

Vim 的内置帮助功能

这节内容不需要死记。建议使用“收藏”功能，这样，你可以在以后碰到不认识的符号标记的时候，返回来查看这一节的内容。

Vim 的选项和配置

了解了 Vim 模式和键描述，我们对 Vim 的认识又多了一些，第一步的学习成就达成。要想更好地使用 Vim，下一个关键点就是配置了，接下来我就带你看看 Vim 配置都有哪些需要注意的点。

作为一个可以越用越顺手的应用程序，Vim 是需要配置的。我们才刚开始学习，所以目前我们的配置文件是相当简单的，但随着课程的进展和你使用 Vim 越来越多，你的 Vim 配置文件必然会越变越复杂。我们今天就先来做一些初步的讨论，看看能实际使用的一个最基本 Vim 配置文件是什么样子。

我们上节课已经讨论过，根据 Unix 下的惯例，Vim 的配置文件放在用户的主目录下，文件名通常是 .vimrc；而它在 Windows 下名字是 _vimrc。我们前面给出最基本的配置文件是这个样子的：

set enc=utf-8
set nocompatible
source $VIMRUNTIME/vimrc_example.vim

如果你熟悉 shell 语法，你肯定能看到不少熟悉的影子在里面。这三行完成了下列功能：

设置编辑文件的内码是 UTF-8（非所有平台缺省，但为编辑多语言文件所必需）

设置 Vim 不需要和 vi 兼容（仅为万一起见，目前大部分情况下这是缺省情况）

导入 Vim 的示例配置（这会打开一些有用的选项，如语法加亮、搜索加亮、命令历史、记住上次的文件位置，等等）

对于现代 Unix 系统上的 Vim 8，实际上只需要最后一句就足够了。对于现代 Windows 系统上的 Vim 8，中间的这句 set nocompatible 也可以删除。如果你在较老的 Vim 版本上进行配置，那么把这三行全放进去会比较安全。

接下来，我会讲一些基本的配置项，保证你的日常工作流顺畅。它们是：备份和跨会话撤销、鼠标支持、中文支持及图形界面的字体支持。除了字体支持主要牵涉到美观性，其他三项都是对编辑至关重要的基本功能。我们一一来看。

备份和撤销文件

上面的基本设置会产生一个有人喜欢、但也有很多人感到困惑的结果：你修改文件时会出现结尾为“~”的文件，有文件名后面直接加“~”的，还有前面加“.”后面加“.un~”的。这是因为在示例配置里，Vim 自动设置了下面两个选项：

set backup
set undofile

前一个选项使得我们每次编辑会保留上一次的备份文件，后一个选项使得 Vim 在重新打开一个文件时，仍然能够撤销之前的编辑（undo），这就会产生一个保留编辑历史的“撤销文件”（undofile）了。

有撤销文件时，再次打开文件时仍然可以撤销上次的编辑

我的通常做法是，不产生备份文件，但保留跨会话撤销编辑的能力；因为有了撤销文件，备份其实也就没有必要了。同时，把撤销文件放在用户个人的特定目录下，既保证了安全，又免去了其他目录下出现不必要文件的麻烦。

要达到这个目的，我在 Linux/macOS 下会这么写：

set nobackup
set undodir=~/.vim/undodir

在 Windows 下这么写：

set nobackup
set undodir=~\vimfiles\undodir

无论哪种环境，你都需要创建这个目录。我们可以用下面的命令来让 Vim 在启动时自动创建这个目录：

if !isdirectory(&undodir)
  call mkdir(&undodir, 'p', 0700)
endif

如果我告诉你， &undodir 代表 undodir 这个选项的值，那么其他代码的基本作用，相信你也一定能看出来了吧？我们暂时就不做进一步分析了。如果你好奇的话，可以提前看一下下面各项的 Vim 帮助文档：

:help isdirectory()

:help mkdir()

:help :call

这个跨会话撤销的能力，我还真不知道其他哪个编辑器也有。更妙的是，Vim 还有撤销树的概念，可以帮助你回到任一历史状态。这个我们以后会和相关的插件一起讨论。

鼠标支持

我不知道你会不会像某些资深 Vim 用户一样，只用键盘不用鼠标。我反正是做不到的，也没有动力去那样做——毕竟，浪费计算机界一项伟大的发明并不那么有必要😂。手一直在键盘上的本位排（home row）打字当然会更快，但一个程序员看代码的时间比写代码的时间要多得多，而在非线性的跳转任务上，鼠标比键盘更加快，也更加有效。

在 Vim 的终端使用场景下，鼠标的选择有一定的歧义：你希望是使用 Vim 的可视模式选择内容，并且只能在 Vim 里使用呢，还是产生 Vim 外的操作系统的文本选择，用于跟其他应用程序的交互呢？这是一个基本的使用问题，两种情况都可能发生，都需要照顾。

如果你使用 xterm 兼容终端的话，通常的建议是：

在不按下修饰键时，鼠标选择产生 Vim 内部的可视选择。

在按下 Shift 时，鼠标选择产生操作系统的文本选择。

对于不兼容 xterm、不支持对 Shift 键做这样特殊处理的终端，我们一般会采用一种“绕过”方式，让 Vim 在某种情况下暂时不接管鼠标事件。通常的选择是在命令行模式下不使用鼠标。下面，我们就分这两种情况来配置。

虽然最新的 Vim 缺省配置文件（示例配置文件会包含缺省配置），在大部分情况下已经可以自动设置合适的鼠标选项了，不过为照顾我们课程的三种不同平台，我们还是手工设置一下：

if has('mouse')
  if has('gui_running') || (&term =~ 'xterm' && !has('mac'))
    set mouse=a
  else
    set mouse=nvi
  endif
endif

上面代码说的是，如果 Vim 有鼠标支持的话，那在以下任一条件满足时：

图形界面正在运行

终端是 xterm 兼容，并且不是 Mac（Mac 上的终端声称自己是 xterm，但行为并不完全相同）

我们将启用完全的鼠标支持（mouse=a）。特别是，此时鼠标拖拽就会在 Vim 里使用可视模式选择内容（只能在 Vim 里使用）。而当用户按下 Shift 键时，窗口系统接管鼠标事件，用户可以使用鼠标复制 Vim 窗口里的内容供其他应用程序使用。

否则（非图形界面的的终端，且终端类型不是 xterm），就只在正常模式（n）、可视模式（v）、插入模式（i）中使用鼠标。这意味着，当用户按下 : 键进入命令行模式时，Vim 将不对鼠标进行响应，这时，用户就可以使用鼠标复制 Vim 窗口里的内容到其他应用程序里去了。

可视模式的选取和按“:”后的选取

非 xterm 的鼠标支持在 macOS 和 Windows 下都有效。但在 Windows 下需要注意的一点是，如果使用非图形界面的 Vim 的话，应当在命令提示符（Command Prompt）的属性里关闭“快速编辑模式”（QuickEdit Mode），否则 Vim 在运行时将无法对鼠标事件进行响应。

鉴于命令提示符的行为有很多怪异和不一致之处，强烈建议你在 Windows 下，要么使用图形界面的 Vim，要么使用 Cygwin/MSYS2 里、运行在 mintty 下的 Vim。

中文支持

接下来我和你讲讲中文支持的问题。如果你一直在 UTF-8 下使用中文的话，那这一小节的内容可以跳过。对于大部分在 Unix 下工作的人员，应该是这样的情况。而如果你在 Windows 上工作，或者有需要跟别人交换 GB2312、GBK、GB18030 编码的文本文件，那这部分的内容还是需要看一下的。

完整的 Unicode 历史和原理可以讲上整整一讲，但从实用的角度，我们就简化成下面几条吧：

整个世界基本上在向 UTF-8 编码靠拢。

微软由于历史原因，内部使用 UTF-16；UTF-16 可以跟 UTF-8 无损转换。

GB2312、GBK、GB18030 是一系列向后兼容的中文标准编码方式，GB2312 编码的文件是合法的 GBK 文件，GBK 编码的文件是合法的 GB18030 文件。但除了 GB18030，都不能做到跟 UTF-8 无损转换；目前非 UTF-8 的简体中文文本基本上都用 GBK/GB18030 编码（繁体中文文本则以 Big5 居多）。鉴于 GB18030 是国家标准，其他两种编码也和 GB18030 兼容，我们就重点讲如何在 Vim 中支持 GB18030 了。

举一个具体的例子，“你好😄”这个字符串，在 UTF-8 编码下是下面 10 个字节（我按字符进行了分组）：

e4bda0 e5a5bd f09f9884

如果使用 GB18030 编码（GB2312/GBK 不能支持表情字符）的话，会编码成 8 个字节：

c4e3 bac3 9439fd30

这么看起来，GB18030 处理中文在存储效率上是优势的。但它也有缺点：

GBK 外的 Unicode 字符一般需要四字节编码（非中文情况会劣化）

GBK 外的 Unicode 字符跟 Unicode 码点需要查表才能转换（UTF-8 则可以用非常简单的条件判断、移位、与、或操作来转换）

一旦出现文件中有单字节发生损毁，后续的所有中文字符都可能发生紊乱（而 UTF-8 可以在一个字符之后恢复）

因此，GB18030 在国际化的软件中不会作为内码来使用，只会是读取 / 写入文件时使用的转换编码。我们要让 Vim 支持 GB18030 也同样是如此。由于 UTF-8 编码是有明显规律的，并非任意文件都能成功地当成 UTF-8 来解码，我们一般使用的解码顺序是：

首先，检查文件是不是有 Unicode 的 BOM（字节顺序标记）字符，有的话按照 BOM 字符来转换文件内容。

其次，检查文件能不能当作 UTF-8 来解码；如果可以，就当作 UTF-8 来解释。

否则，尝试用 GB18030 来解码；如果能成功，就当作 GB18030 来转换文件内容。

最后，如果上面的解码都不成功，就按 Latin1 字符集来解码；由于这是单字节的编码，转换必定成功。

事实上，Vim 缺省差不多就是按这样的顺序，但第三步使用何种编码跟系统配置有关。如果你明确需要处理中文，那在配置文件里最好明确写下下面的选项设定：

set fileencodings=ucs-bom,utf-8,gb18030,latin1

图形界面的字体配置

图形界面的 Vim 可以自行配置使用的字体，但在大部分环境里，这只是起到美化作用，而非必需项。不过，对于高分辨率屏幕的 Windows，这是一个必需项：Vim 在 Windows 下缺省使用的不是 TrueType 字体，不进行配置的话，字体会小得没法看。

高分辨率屏下的 Windows 图形界面 Vim

在 Windows 的缺省字体里，一般而言，Consolas 和 Courier New 还比较合适。以 Courier New 为例，在 _vimrc 里可以这样配置（Windows 上的基本写法是字体名称加冒号、“h”加字号；用“_”取代空格，否则空格需要用“\”转义）：

if has('gui_running')
  set guifont=Courier_New:h10
endif

设置了 10 磅 Consolas 字体的 Vim

字体名称如何写是件平台相关的事（可参见帮助文档“:help gui-font”）。如果你不确定怎么写出你需要的字体配置，或者你怎么写都写不对的话，可以先使用图形界面的菜单来选择（通常是“编辑 > 选择字体”；在 MacVim 里是“Edit > Font > Show Fonts”），然后使用命令“:set guifont?”来查看。

注意，Vim 在设置选项时，空格需要用“\”进行转义。比如，如果我们要在 Ubuntu 下把字体设成 10 磅的 DejaVu Sans Mono，就需要写：

  " Linux 和 Windows 不同，不能用 '_' 取代空格
  set guifont=DejaVu\ Sans\ Mono\ 10

此外，宽字符字体（对我们来讲，就是中文字体了）是可以单独配置的。这可能就更是一件仁者见仁、智者见智的事了。对于纯中文的操作系统，这一般反而是不需要配置的；但如果你的语言设定里，中文不是第一选择的话，就有可能在显示中文时出现操作系统误用日文字体的情况。这时你会想要手工选择一个中文字体，比如在 Ubuntu 下，可以用：

  set guifontwide=Noto\ Sans\ Mono\ CJK\ SC\ 11

注意，在不同的中英文字体搭配时，并不需要字号相同。事实上，在 Windows 和 Linux 上我通常都是使用不同字号的中英文字体的。

Ubuntu 下的 gvim 设置中文字体

在上面的动图中，你可以观察到设了中文字体之后，不仅中文字变大，更美观了，“将”、“适”、“关”、“复”、“启”等字的字形也同时发生了变化。

由于字体在各平台上差异较大，字体配置我就不写到 Vim 的参考配置中去了，只把如何选择和配置的方法写出来供你参考。

内容小结

好了，这一讲我就讲到这里，我们来做个内容小结。

今天我给出了一张键盘图，带你复习了 Vim 教程的内容，这里我要再强调一遍，这部分的内容如果你还有不熟悉的，一定要再去学习一下 Vim 教程，这段时间我们一定要多花点时间和精力来练习，把这一步跨过去。

掌握了 Vim 教程里的基础信息还远远不够，我们还得了解 Vim 的四种主要模式，你只要记住最重要的就是正常模式就可以了。

最后我带你学习了 Vim 的几个基本配置选项，包括对撤销、鼠标、中文和字体的支持，来满足最基本的编辑需要。最终的 Vim 配置文件可以在 GitHub 上找到：

https://github.com/adah1972/geek_time_vim

关于这个配置文件，我这里做个备注说明：主（master）分支可以用在类 Unix 平台上，windows 分支则用在 Windows 上。适用于今天这一讲的内容标签是 l2-unix 和 l2-windows：你可以用 git checkout l2-unix 或 git checkout l2-windows 来得到相应平台对应本讲的配置文件。

好了，掌握了今天的内容，你就可以用 Vim 做最基本的编辑了。

课后练习

请使用本讲的配置文件，并尝试以下操作：

退出 Vim 然后重新打开文件，仍然可以撤销上次的编辑。

使用终端的 Vim，在终端里用鼠标复制 Vim 里的文本到另外一个文本编辑器中（仅 Unix 下，可选）。

在 Vim 中使用“:help”命令（大部分环境下也可以使用 <F1> 功能键），尝试查看命令说明，以及使用键盘和鼠标在帮助主题中跳转。

如果遇到任何问题，欢迎留言和我讨论。我们下一讲见。

03｜更多常用命令：应对稍复杂的编辑任务

你好，我是吴咏炜。

上一讲我们通过 Vim 教程学习了 Vim 的基本命令，我还给你讲解了 Vim 的基本配置，现在你就已经可以上手基本的编辑工作了。

今天，我们将学习更多 Vim 的常用命令，以便更高效地进行编辑。我会先带你过一下光标移动命令和文本修改命令，然后重点讲解文本对象，随后快速讨论一下不能搭配文本修改的光标移动命令，最后讨论如何重复命令。

光标移动

我们先来讨论一下可以跟文本修改搭配的光标移动命令。

通过前面的课程，你已经知道，Vim 里的基本光标移动是通过 h、j、k、l 四个键实现的。之所以使用这四个键，是有历史原因的。你看一下 Bill Joy 开发 vi 时使用的键盘就明白了：这个键盘上没有独立的光标键，而四个光标符号直接标注在 H、J、K、L 四个字母按键上。

Lear Siegler ADM-3A 终端键盘的排布（图片源自维基百科）

当然，除了历史原因外，这四个键一直使用至今，还是有其合理性的。它们都处于打字机的本位排（home row）上，这样打字的时候，手指基本不用移动就可以敲击到。因此，即使到了键盘上全都有了光标移动键的今天，很多 Vim 的用户仍然会使用这四个键来移动光标。

不过，标准的光标移动键可以在任何模式下使用，而这四个键并不能在插入模式下使用，因此，它们并不构成完全的替代关系。

顺便提一句，你有没有注意到 ADM-3A 键盘上的 Esc 键在今天 Tab 的位置？在 Bill Joy 决定使用 Esc 来退出插入模式的时候，Esc 在键盘上的位置还没像今天那样跑到遥远的左上角去……

Vim 跳转到行首的命令是 0，跳转到行尾的命令是 $，这两个命令似乎没什么特别的原因，一般用 <Home> 和 <End> 也没什么不方便的，虽然技术上它们有一点点小区别。如果你感兴趣、想进一步了解的话，可以参考帮助 :help <Home>。此外，我们也有 ^，用来跳转到行首的第一个非空白字符。

对于一次移动超过一个字符的情况，Vim 支持使用 b/w 和 B/W，来进行以单词为单位的跳转。它们的意思分别是 words Backward 和 Words forward，用来向后或向前跳转一个单词。小写和大写命令的区别在于，小写的跟编程语言里的标识符的规则相似，认为一个单词是由字母、数字、下划线组成的（不严格的说法），而大写的命令则认为非空格字符都是单词。

小写 w 和大写 W 的区别

根据单个字符来进行选择也很常见。比如，现在光标在 if (frame->fr_child != NULL) 第五个字符上，如果我们想要修改括号里的所有内容，需要仔细考虑 w 的选词规则，然后输入 c5w 吗？这样显然不够方便。

这种情况下，我们就需要使用 f（find）和 t（till）了。它们的作用都是找到下一个（如果在输入它们之前先输入数字 n 的话，那就是下面第 n 个）紧接着输入的字符。两者的区别是，f 会包含这个字符，而 t 不会包含这个字符。在上面的情况下，我们用 t 就可以了：ct) 就可以达到目的。如果需要反方向搜索的话，使用大写的 F 和 T 就可以。

对于写文字的情况，比如给开源项目写英文的 README，下面的光标移动键也会比较有用：

( 和 ) 移到上一句和下一句

{ 和 } 移到上一段和下一段

整句和整段的移动

在很多环境（特别是图形界面）里，Vim 支持使用 <C-Home> 和 <C-End> 跳转到文件的开头和结尾。如果遇到困难，则可以使用 vi 兼容的 gg 和 G 跳转到开头和结尾行（小区别：G 是跳转到最后一行的第一个字符，而不是最后一个字符）。

光标移动咱们就讲到这里。你需要重点掌握的就是 Vim 里除了简单的光标移动，还有“小词”、“大词”、句、段的移动，以及字符的搜索；每种方式都分向前和向后两种情况。

文本修改

接着，我们来看文本修改。

在 Vim 的教程里，我们已经学到，c 和 d 配合方向键，可以对文本进行更改。本质上，我们可以认为 c（修改）的功能就是执行 d（删除）然后 i（插入）。在 Vim 里，一般的原则就是，常用的功能，按键应尽可能少。因此很多相近的功能在 Vim 里会有不同的按键。不仅如此，大写键也一般会重载一个相近但稍稍不同的含义：

d 加动作来进行删除（dd 删除整行）；D 则相当于 d$，删除到行尾。

c 加动作来进行修改（cc 修改整行）；C 则相当于 c$，删除到行尾然后进入插入模式。

s 相当于 cl，删除一个字符然后进入插入模式；S 相当于 cc，替换整行的内容。

i 在当前字符前面进入插入模式；I 则相当于 ^i，把光标移到行首非空白字符上然后进入插入模式。

a 在当前字符后面进入插入模式；A 相当于 $a，把光标移到行尾然后进入插入模式。

o 在当前行下方插入一个新行，然后在这行进入插入模式；O 在当前行上方插入一个新行，然后在这行进入插入模式。

r 替换光标下的字符；R 则进入替换模式，每次按键（直到 <Esc>）替换一个字符。

u 撤销最近的一个修改动作；U 撤销当前行上的所有修改。

熟练掌握这些按键需要一定的记忆和练习。但是，当你熟练掌握之后，大部分编辑操作只需要按一两个按键就能完成；而在你还没有做到熟练掌握之前，记住最简单、最有逻辑的按键也可以让你至少能够完成需要的编辑任务。

文本对象选择

好，接下来就是我们今天的重点内容，文本对象的选择了。我之所以把这部分内容作为这节课的重点，是因为这是一个很方便很强大的功能，并且特别适合程序中的逻辑块的编辑。

到现在，我们已经学习过，可以使用 c、d 加动作键对这个动作选定的文本块进行操作，也可以使用 v 加动作键来选定文本块（以便后续进行操作），我们也学习了好些移动光标的动作。不过，还有几个动作只能在 c、d、v 这样命令之后用，我们也需要学习一下。

这些选择动作的基本附加键是 a 和 i。其中，a 可以简单理解为英文单词 a，表示选定后续动作要求的完整内容，而 i 可理解为英文单词 inner，代表后续动作要求的内容的“内部”。这么说，还是有点抽象，我们来看一下具体的例子。

假设有下面的文本内容：

if (message == "sesame open")

我们进一步假设光标停在“sesame”的“a”上，那么：

dw（理解为 delete word）会删除 ame␣，结果是 if (message == "sesopen")

diw（理解为 delete inside word）会删除 sesame，结果是 if (message == " open")

daw（理解为 delete a word）会删除 sesame␣，结果是 if (message == "open")

diW 会删除 "sesame，结果是 if (message == open")

daW 会删除 "sesame␣，结果是 if (message == open")

di" 会删除 sesame open，结果是 if (message == "")

da" 会删除 "sesame open"，结果是 if (message ==)

di( 或 di) 会删除 message == "sesame open"，结果是 if ()

da( 或 da) 会删除 (message == "sesame open")，结果是 if␣

上面演示了 a、i 和 w、双引号、圆括号搭配使用，这些对于任何语言的代码编辑都是非常有用的。实际上，可以搭配的还有更多：

搭配 s（sentence）对句子进行操作——适合西文文本编辑

搭配 p（paragraph) 对段落进行操作——适合西文文本编辑，及带空行的代码编辑

搭配 t（tag）对 HTML/XML 标签进行操作——适合 HTML、XML 等语言的代码编辑

搭配 ` 和 ' 对这两种引号里的内容进行操作——适合使用这些引号的代码，如 shell 和 Python

搭配方括号（“[”和“]”）对方括号里的内容进行操作——适合各种语言（大部分都会用到方括号吧）

搭配花括号（“{”和“}”）对花括号里的内容进行操作——适合类 C 的语言

搭配角括号（“<”和“>”）对角括号里的内容进行操作——适合 C++ 的模板代码

再进一步，在 a 和 i 前可以加上数字，对多个（层）文本对象进行操作。下面图中是一个示例：

修改往上第 2 层花括号内的所有内容

你看，无论你使用什么语言，这些快捷的文本对象选择方式是不是总会有一种可以适用？我个人觉得这些功能绝对是 Vim 的强项了，所以，我再敲一次黑板，这部分内容是重点，不要嫌内容多，挨个儿用一用、练一练，你会发现这个功能非常实用，在写代码的时候常常会用得上。

更快地移动

除了这讲开头提到的光标移动功能外，还有一些通常不和操作搭配的光标和屏幕移动功能。我们在这节里会快速描述一下。

我们仍然可以使用 <PageUp> 和 <PageDown> 来翻页，但 Vim 更传统的用法是 <C-B> 和 <C-F>，分别代表 Backward 和 Forward。

除了翻页，Vim 里还能翻半页，有时也许这种方式更方便，需要的键是 <C-U> 和 <C-D>，Up 和 Down。

如果你知道出错位置的行号，那你可以用数字加 G 来跳转到指定行。类似地，你可以用数字加 | 来跳转到指定列。这在调试代码的时候非常有用，尤其适合进行自动化。

下图中展示了 iTerm2 中捕获输出并执行 Vim 命令的过程（用 vim -c 'normal 5G36|' 来执行跳转到出错位置第 5 行第 36 列）：

捕获错误信息并自动通过 Vim 命令行来跳转到指定位置

（如果你用 iTerm2 并对这个功能感兴趣，我设置的正则表达式是 ^([_a-zA-Z0-9+/.-]+):([0-9]+):([0-9]+): (?:fatal error|error|warning|note):，捕获输出后执行的命令是 echo "vim -c 'normal \2G\3|' \1"。）

你只关心当前屏幕的话，可以快速移动光标到屏幕的顶部、中间和底部：用 H（High）、M（Middle）和 L（Low）就可以做到。

顺便提一句，vimrc_example 有一个设定，我不太喜欢：它会设 set scrolloff=5，导致只要屏幕能滚动，光标就移不到最上面的 4 行和最下面的 4 行里，因为一移进去屏幕就会自动滚动。这同样也会导致 H 和 L 的功能发生变化：本来是移动光标到屏幕的最上面和最下面，现在则变成了移动到上数第 6 行和下数第 6 行，和没有这个设定时的 6H 与 6L 一样了。所以我一般会在 Vim 配置文件里设置 set scrolloff=1（你也可以考虑设成 0），减少这个设置的干扰。

只要光标还在屏幕上，你也可以滚动屏幕而不移动光标（不像某些其他编辑器，Vim 不允许光标在当前屏幕以外）。需要的按键是 <C-E> 和 <C-Y>。

另外一种可能更实用的滚动屏幕方式是，把当前行“滚动”到屏幕的顶部、中部或底部。Vim 里的对应按键是 zt、zz 和 zb。和上面的几个滚动相关的按键一样，它们同样受选项 scrolloff 的影响。

光标移动和屏幕滚动

重复，重复，再重复

今天的最后，我来带你解决一个你肯定会遇到的问题，那就是如何更高效地解决重复的操作。

我们已经看到，在 Vim 里有非常多的命令，而且很多命令都需要敲好几个键。如果你要重复这样的命令，每次都要再手敲一遍，这显然是件很费力的事。作为追求高效率的编辑器，这当然是不可接受的。除了我们以后要学到的命令录制、键映射、自定义脚本等复杂操作外，Vim 对很多简单操作已经定义了重复键：

; 重复最近的字符查找（f、t 等）操作

, 重复最近的字符查找操作，反方向

n 重复最近的字符串查找操作（/ 和 ?）

N 重复最近的字符串查找操作（/ 和 ?），反方向

. 重复执行最近的修改操作

有了这些，重复操作就非常简单了。要掌握它们的方法就是多练习，多用几次自然就会了。

内容小结

好了，今天的内容就讲完了，我们来做个小结。我们讨论了更多的一些常用 Vim 命令，包括：

基本光标移动命令（可配合 c、d 和 v）

文本修改命令小汇总

文本对象命令（c、d、v 后的a 和 i）

更快的光标和屏幕移动功能

重复功能

今天讲的内容不难，重点是文本对象。你知道吗？我见到的 Vim 命令速查表里通常也没有它们，因而连很多 Vim 的老用户都不知道这些功能呢。所以，掌握了这部分内容，我们就已经走在很多 Vim 用户的前面了。请一定要多加练习，用好这个功能会大大提升你的代码编辑效率。

最后，提醒你去 GitHub 上看配置文件。配置文件我们有一处改动。类似地，适用于本讲的内容标签是 l3-unix 和 l3-windows。

课后练习

请把本讲里面描述的 Vim 功能自己练习一下，尤其需要重点掌握的是文本修改命令、文本对象命令和重复功能。其他某些功能可能只对部分人和某些场景有用，如果一个功能你觉得用不上，不用去强记。毕竟，不用的功能，即使一时死记硬背可以记住，也很快会遗忘的。

欢迎你在留言区分享自己的学习收获和心得，有问题也要及时反馈，我们一起交流讨论。我们下一讲见！

04｜初步定制：让你的 Vim 更顺手

你好，我是吴咏炜。

在前几讲，我已经介绍了不少 Vim 的常用命令，我想你已经略有心得了吧。今天我们转换一下视角，来讲一下 Vim 这个软件本身。

作为一个 Vim 的使用者，光熟悉命令是不够的，你还需要定制 Vim。因为每个人的习惯和需求都是不一样的，一个高度定制化的 Vim 环境能大大提高你的工作效率。

今天，我会先带你了解一下 Vim 的运行支持文件目录结构，然后我们再一起探索 Vim 8 带来的新功能，及如何对 Vim 进行初步配置来使得 Vim 更加好用。

Vim 的目录结构

Vim 的工作环境是由运行支持文件来设定的。如果你想要定制 Vim，就要熟知 Vim 有哪些不同类型的运行支持文件，分别存放在哪里，怎样能快捷地找到它们。Vim 比较有意思的一点的是，虽然运行支持文件是在 Vim 的安装目录下，但用户自己是可以“克隆”这个目录结构的。也就是说，你自己目录下的用户配置，到你深度定制的时候，也有相似的目录结构。所以，我就先从这些文件的目录结构开始讲起。

安装目录下的运行支持文件

Vim 的运行支持文件在不同的平台上有着相似的目录结构。以 Vim 8.2 为例，它们的标准安装位置分别在：

大部分 Unix 下面：/usr/share/vim/vim82

macOS Homebrew 下：/usr/local/opt/macvim/MacVim.app/Contents/Resources/vim/runtime

Windows 下：C:\Program Files (x86)\Vim\vim82

在这个目录下面，你可以看到很多子目录，如 autoload、colors、doc、pack、plugin、syntax 等等。这些子目录下面就是分类放置的 Vim 支持文件。最常用的子目录应该是下面这几个：

syntax：Vim 的语法加亮文件

doc：Vim 的帮助文件

colors：Vim 的配色方案

plugin：Vim 的“插件”，即用来增强 Vim 功能的工具

以 syntax 目录为例，当前我在下面看到有 617 个文件，也就是说，Vim 对 617 种不同的文件类型提供了语法加亮支持！这里面的文件去掉“.vim”后缀后，就是文件类型的名字，你可以用类似 :setfiletype java 这样的命令来设置文件的类型，从而进行语法加亮。目录下我们可以看到大家都很熟悉的语言，也有很多我从来都没听说过的东西。

只要有正当的理由，你就可以向 Vim 的作者 Bram 提交改进版本，或是对全新的语言的支持。我就对若干种文件类型提交过补丁，新增了对《计算机编程艺术》中的 MIX 汇编语言的语法支持，并维护着微软宏汇编（MASM）的语法文件。

在图形界面的 Vim 里，你可以通过“语法 > 在菜单中显示文件类型”（“Syntax > Show File Types in Menu”）来展示 Vim 的所有文件类型，然后可以选择某一类型来对当前文件进行设置。这儿的菜单项，跟 syntax 目录下的文件就基本是一一对应的了。

在菜单中显示文件类型这个额外的步骤，可能是因为很久很久以前，加载所有文件类型的菜单是一个耗时的操作吧。在 menu.vim 里，目前有这样的代码：

" Skip setting up the individual syntax selection menus unless
" do_syntax_sel_menu is defined (it takes quite a bit of time).
if exists("do_syntax_sel_menu")
  runtime! synmenu.vim
else
  …
endif

不知道这段注释是什么年代加上的……但显然，我们的电脑已经不会再在乎加载几百个菜单项所占的时间了。即使我不怎么用菜单，我也找不出不直接展示这个菜单的理由；我可不想在需要使用的时候再多点一次鼠标。

所以，我会在我的 vimrc 文件里写上：

let do_syntax_sel_menu = 1

同理，我会加载其他一些可能会被 Vim 延迟加载的菜单，减少需要在菜单上点击的次数：

let do_no_lazyload_menus = 1

上面两个设置在我的机器上会让我打开 Vim 的速度下降大概 20 毫秒。我想我不在乎这点时间差异……

我们用“:help”命令查看的帮助文件就放在 doc 目录下。我们可以用菜单“编辑 > 配色方案”（“Edit > Color Scheme”）浏览配色方案，相应的文件就在 colors 目录下。

在 plugin 目录下的系统内置插件不多，我们下面就快速讲解一下：

getscriptPlugin：获得最新的 Vim 脚本的插件（在目前广泛使用 Git 的年代，这个插件过时了，我们不讲）

gzip：编辑 .gz 压缩文件（能在编辑后缀为 .gz 的文件时自动解压和压缩，你会感觉不到这个文件是压缩的）

logiPat：模式匹配的逻辑运算符（允许以逻辑运算、而非标准正则表达式的方式来写模式匹配表达式）

manpager：使用 Vim 来查看 man 帮助（强烈建议试一下，记得使用 Vim 的跳转键 C-] 和 C-T）

matchparen：对括号进行高亮匹配（现代编辑器基本都有类似的功能）

netrwPlugin：从网络上编辑文件和浏览（远程）目录（支持多种常见协议如 ftp 和 scp，可直接打开目录来选择文件）

rrhelper：用于支持 --remote-wait 编辑（Vim 的多服务器会用到这一功能）

spellfile：在拼写文件缺失时自动下载（Vim 一般只安装了英文的拼写文件）

tarPlugin：编辑（压缩的）tar 文件（注意，和 gzip 情况不同，这儿不支持写入）

tohtml：把语法加亮的结果转成 HTML（自己打开个文件，输入命令“:TOhtml”就知道效果了）

vimballPlugin：创建和解开 .vba 文件（这个目前也略过时了，我们不讲）

zipPlugin：编辑 zip 文件（和 tar 文件不同，zip 文件可支持写入）

除了 rrhelper 和 spellfile 属于功能支持插件，没有自己的帮助页面，其他功能都可以使用“:help”命令来查看帮助。查看帮助时，插件名称中的“Plugin”后缀需要去掉：查看 zip 文件编辑的帮助时，应当使用“:help zip”而不是“:help zipPlugin”。

从这些插件当中，我们已经可以看到 Vim 的一些特殊威力了吧。下面的动图里，我们可以看到部分插件功能的展示：

浏览远程目录，打开一个 tar.gz 文件

使用 Vim 查看 man 帮助

用户的 Vim 配置目录

Vim 的安装目录你是不应该去修改的。首先，你可能没有权限去修改这个目录；其次，即使你有修改权限，这个目录会在 Vim 升级时被覆盖，你做的修改也会丢失。用户自己的配置应当放在自己的目录下，这也就是用户自己的主目录下的 Vim 配置目录（Unix 下的 .vim，Windows 下的 vimfiles）。这个目录应和 Vim 安装目录下的运行支持文件目录有相同的结构，但下面的子目录你在需要修改 Vim 的相关行为时才有必要创建。如果一个同名文件出现用户自己的 Vim 配置目录里和 Vim 的安装目录里，用户的文件优先。

换句话说，修改 Vim 行为最简单的一种方式，就是把一个系统的运行支持文件复制到自己的 Vim 配置目录下的相同位置，然后修改其内容。我自己常常用这种方式来精调 Vim 的语法加亮。

显然，这种方式的缺点（在适当的时候也是优点）是，如果 Vim 的运行支持文件后来被修改 / 更新了，你也会继续使用你自己目录下的老版本修改版。如果你的修改不只是你自己的临时方案、同时也适合他人的话，最佳做法还是给 Vim 项目提交补丁，让其他所有人都能用上你的修改，这样才是开源的最佳使用方式。

关于 Vim 的公用脚本，这儿再多说几句。Vim 的网站过去是用来集中获取各种脚本——如插件和配色方案——的地方，而 getscriptPlugin 可以帮助简化这个过程。今天你仍然可以使用这个方法，但 Git 和 GitHub 的广泛使用已经改变了人们获取和更新脚本的方式。现在，最主流的分发 Vim 脚本的方式是利用 GitHub，而用户则使用包管理器来调用 Git 从 GitHub（或类似的 Git 库）获取和更新脚本，下面我们很快就会讲到。

理解了 Vim 的目录结构，我们接着来看 Vim 8 的新功能。

Vim 8 新功能

Vim 是一个持续改进中的应用程序。从 Vim 8.1（2018 年 5 月 17 日）到 Vim 8.2（2019 年 12 月 12 日），Vim 有 2424 个补丁，也就是说，平均每天超过 4 个补丁。很多 Vim 8 里的大功能，并不是一次性引入，而是在补丁中慢慢引入的。比如，Vim 里现在有终端支持，这个功能从 Vim 8.0.0693 开始引入，到了 Vim 8.1，成为一个正式的大功能。

站在我个人的角度看，从 Vim 7.4 到 Vim 8.2，最大的新功能是：

Vim 软件包的支持（“:help packages”）

异步任务支持（“:help channel”、“:help job”和“:help timers”）

终端支持（“:help terminal”）

今天我们就重点讲一下 Vim 软件包。这是 Vim 8 里带来的一个重要功能，也让我们在扩展 Vim 的时候变得更方便了。

Vim 软件包

Vim 的目录结构有点传统 Unix 式：一个功能用到的文件可能会分散在多个目录下。就像传统 Unix 上 Vim 的文件可能分散在 /usr/bin、/usr/share/man、/usr/share/vim 等目录下一样，一个 Vim 的插件（严格来讲，应该叫包）通常也会分散在多个目录下：

插件的主体通常在 plugin 目录下

插件的帮助文件在 doc 目录下

有些插件只对某些文件类型有效，会有文件放在 ftplugin 目录下

有些插件有自己的文件类型检测规则，会有文件放在 ftdetect 目录下

有些插件有特殊的语法加亮，会有文件放在 syntax 目录下

……

以前我们安装插件，一般是一次性安装后就不管了。安装过程基本上就是到 .vim 目录（Windows 上是 vimfiles 目录）下，解出压缩包的内容，然后执行 vim -c 'helptags doc|q' 生成帮助文件的索引。到了“互联网式更新”的年代，这种方式就显得落伍了。尤其糟糕的地方在于，它是按文件类型来组织目录的，而不是按相关性，这就没法用 Git 来管理了。

Vim 上后来就出现了一些包管理器，它们的基本模式都是相通的：每个包有自己的目录，然后这些目录会被加到 Vim 的运行时路径（runtimepath）选项里。最早的 runtimepath 较为简单，在 Unix 上缺省为：

$HOME/.vim,

$VIM/vimfiles,

$VIMRUNTIME,

$VIM/vimfiles/after,

$HOME/.vim/after

而在有了包管理器之后，runtimepath 就会非常复杂，每个包都会增加一个自己的目录进去。但是，好处也是非常明显的，包的管理变得非常方便。从 Vim 8 开始，Vim 官方也采用了类似的体系。Vim 会在用户的配置目录（Unix 下是 $HOME/.vim ，Windows 下是 $HOME/vimfiles ）下识别名字叫 pack 的目录，并在这个目录的子目录的 start 和 opt 目录下寻找包的目录。

听着有点绕吧？我们看一个实际的 Vim 配置目录的结构就清楚了：

.
├── colors
├── doc
├── pack
│   ├── minpac
│   │   ├── opt
│   │   │   ├── minpac
│   │   │   ├── vim-airline
│   │   │   └── vimcdoc
│   │   └── start
│   │       ├── VimExplorer
│   │       ├── asyncrun.vim
│   │       ├── fzf.vim
│   │       ├── gruvbox
│   │       ├── killersheep
│   │       ├── nerdcommenter
│   │       ├── nerdtree
│   │       ├── tagbar
│   │       ├── undotree
│   │       ├── vim-fugitive
│   │       ├── vim-matrix-screensaver
│   │       ├── vim-rainbow
│   │       ├── vim-repeat
│   │       ├── vim-rhubarb
│   │       └── vim-surround
│   └── my
│       ├── opt
│       │   ├── YouCompleteMe
│       │   ├── ale
│       │   ├── clang_complete
│       │   ├── cvsmenu
│       │   └── syntastic
│       └── start
│           ├── vim-gitgutter
│           └── ycmconf
├── plugin
├── syntax
└── undodir

可以看到，pack 目录下有 minpac 和 my 两个子目录（这些名字 Vim 不关心），每个目录下面又有 opt 和 start 两个子目录，再下面就是每个包自己的目录了，里面又可以有自己的一套 colors、doc、plugin 这样的子目录，这样就方便管理了。Vim 8 在启动时会加载所有 pack/*/start 下面的包，而用户可以用 :packadd 命令来加载某个 opt 目录下的包，如 :packadd vimcdoc 命令可加载 vimcdoc 包，来显示中文帮助信息。

有了这样的目录结构，用户要自己安装、管理包就方便多了。不过，我们还是推荐使用一个包管理器。包管理器可以带来下面的好处：

根据文本的配置（一般写在 vimrc 配置文件里）决定要安装哪些包

自动化安装、升级和卸载，包括帮助文件的索引生成

在我们这门课程里，我会使用 minpac，一个利用 Vim 8 功能的小巧的包管理器。如果你已经在使用其他包管理器，我接下来讲的两个小节你可以考虑跳过。

安装 minpac

根据 minpac 网页上的说明，我们在 Windows 下可以使用下面的命令：

cd /d %USERPROFILE%
git clone https://github.com/k-takata/minpac.git ^
    vimfiles\pack\minpac\opt\minpac

在 Linux 和 macOS 下则可以使用下面的命令：

git clone https://github.com/k-takata/minpac.git \
    ~/.vim/pack/minpac/opt/minpac

然后，我们在 vimrc 配置文件中加入以下内容（先不用理解其含义）：

if exists('*minpac#init')
  " Minpac is loaded.
  call minpac#init()
  call minpac#add('k-takata/minpac', {'type': 'opt'})

  " Other plugins
endif

if has('eval')
  " Minpac commands
  command! PackUpdate packadd minpac | source $MYVIMRC | call minpac#update('', {'do': 'call minpac#status()'})
  command! PackClean  packadd minpac | source $MYVIMRC | call minpac#clean()
  command! PackStatus packadd minpac | source $MYVIMRC | call minpac#status()
endif

存盘、重启 Vim 之后，我们就有了三个新的命令，可以用来更新（安装）包、清理包和检查当前包的状态。

通过 minpac 安装扩展包

下面我们就来试验一下通过 minpac 来安装扩展包。我们在“Other plugins”那行下面加入以下内容：

call minpac#add('tpope/vim-eunuch')

保存文件，然后我们使用 :PackUpdate 命令。略微等待之后，我们就能看到类似下面的界面：

插件安装之后的状态界面

这就说明安装成功了。我们可以按 q 来退出这个状态窗口。

我们也可以使用 :PackStatus 来重新打开这个状态窗口。要删除一个插件，在 vimrc 中删除对应的那行，保存，然后使用 :PackClean 命令就可以了。愿意的话，你现在就可以试一下。

最近使用的文件

安装好 Vim 软件包之后，我们进一步来实现一个小功能。

Vim 的缺省安装缺了一个很多编辑器都有的功能：最近使用的文件。

我们就把这个功能补上吧。你只需要按照上一节的步骤安装 yegappan/mru 包就可以了（MRU 代表 most recently used）。安装完之后，重新打开 vimrc 文件，你就可以在图形界面里看到下面的菜单了：

最近文件的菜单

估计你很可能会问：如果是远程连接，没有图形界面怎么办？

我们仍可以在文本界面上唤起菜单，虽然美观程度会差点。你需要在 vimrc 配置文件中加入以下内容（同样，我们暂时先不用去理解其意义）：

if !has('gui_running')
  " 设置文本菜单
  if has('wildmenu')
    set wildmenu
    set cpoptions-=<
    set wildcharm=<C-Z>
    nnoremap <F10>      :emenu <C-Z>
    inoremap <F10> <C-O>:emenu <C-Z>
  endif
endif

在增加上面的配置之后，你就可以使用键 <F10>（当然你也可以换用其他键）加 <Tab> 来唤起 Vim 的文本菜单了。如下图所示：

文本菜单的使用

内容小结

本讲我们讨论了 Vim 8 下的基本目录结构和 Vim 8 的软件包。

你需要知道，Vim 的运行支持目录和用户配置目录有相似的结构，用户配置目录下的文件优先于 Vim 安装目录下的文件。因此，在用户配置目录里进行修改，可以在 Vim 版本有变化时保留自己的定制行为。

而 Vim 8 的软件包使得维护 Vim 的扩展变得更为容易，每一个 Vim 软件包都是独立的目录，可以单独安装、修改和升级。包管理器，如 minpac，则可以帮助我们更方便地安装和管理 Vim 软件包。

对于配置文件，适用于本讲的内容标签是 l4-unix 和 l4-windows。

课后练习

请仔细查看一下你的 Vim 安装目录下面的目录结构，对 Vim 的目录结构有一个直观的理解。你也可以打开一些 .vim 文件来看看，初步感受一下 Vim 脚本：如 filetype.vim 里有着 Vim 如何识别文件类型的逻辑，值得大致了解一下——Vim 并不仅仅是用后缀来判断文件类型的！

另外，一定要根据文中的说明，实践一下安装 minpac，及使用 minpac 来安装其他 Vim 软件包（除非你已经在使用另外一个包管理器了）。我们后面会经常性地安装新的软件包，这一部分操作一定需要牢牢掌握。

如有任何问题，请留言和我交流、讨论。我们下一讲见！

05｜多文件打开与缓冲区：复制粘贴的正确姿势

你好，我是吴咏炜。

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

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

单文件的打开方式

图形界面

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

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

正常模式 Vim 命令 "+P（意义我们后面再解释）

快捷键 <D-V>（提醒：这是我们对 ⌘V 的标记方式；仅适用于 macOS）或 <S-Insert>（PC 键盘）

鼠标右键加“粘贴”（Paste）

菜单“编辑 > 粘贴”（Edit > Paste）

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

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

终端 Vim

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

在终端里进入到目标目录下

使用 vim 文件名 来逐一打开需要编辑的文件

如果光标不在开头的话，用鼠标或 gg 命令跳转到开头

使用命令 i 进入插入模式

使用终端窗口的粘贴命令或快捷键（如 <S-Insert>）来粘贴内容

按 <Esc> 回到正常模式并用 ZZ 存盘退出

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

打开文件使用 :e 文件名；可以使用 <C-D> 来查看有哪些文件，及用 <Tab> 进行自动完成

存盘使用 :w

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

Vim 老版本的特殊处理

老版本 Vim 下直接粘贴可能出现的错误结果

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

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

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

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

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

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

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

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

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

“已经存在交换文件！”

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

你上次编辑这个文件时，发生了意外崩溃。

你已经在使用另外一个 Vim 会话编辑这个文件了。

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

上次编辑这个文件时发生了意外崩溃的错误提示

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

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

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

文件正在其他地方被编辑的错误提示

这时，大部分情况侠我们应当使用 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 命令，在命令行模式下执行正常模式命令，下面就可以直接重复切换命令加粘贴命令，我们的编辑效率也得以大大提升。

第 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 里，除了切换到下一个文件这样的批处理操作外，操作缓冲区的命令比简单操作文件的命令更为方便。

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

文件列表命令 :args 的结果

缓冲区列表命令 :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 打开多个文件，逐个查看，然后退出。

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

06｜窗口和标签页：修改、对比多个文件的正确姿势

你好，我是吴咏炜。

上一讲我们讨论了多文件的编辑。不过，迄今为止，我们即使编辑多个文件，也是在单个窗口里进行的。这样做的局限在于，我们既不能同时修改两个文件，也不能在单个 Vim 会话里对比显示两个文件。当然了，在两个 Vim 会话里倒是可以做到，但有很多不足之处，其中之一就是容易出现“已经存在交换文件”这样的冲突。

所以，这一讲我们就来讨论一下如何利用多窗口、多标签页编辑来实现这些功能。

多窗口编辑

Vim 有窗口的概念。事实上，如果你使用过 Vim 的帮助功能的话，那你就已经见过 Vim 的多窗口界面了。在那种情况下，Vim 自动打开了一个水平分割的帮助窗口。

那如果我们想要自己同时查看、编辑多个文件呢？最基本的命令就是 :split（缩写 :sp）了。这个命令后面如果有文件名，表示分割窗口并打开指定的文件；如果没有文件名，那就表示仅仅把当前窗口分割开，当前编辑的文件在两个窗口里都显示。跟显示帮助文件一样，:split 默认使用水平分割的方式。

既然我说了“水平分割”，聪明如你，一定想到了还有竖直分割。

确实如此。由于 Vim 经常是在终端窗口里打开，而终端宽度常常不能允许在竖直分割时显示两个文件，所以 Vim 默认分割是水平方式。竖直分割要求屏幕比较宽，但如果你想对比两个文件时，竖直分割就会更方便了。

我们可以在会产生分割的命令（如 help 和 split）之前加上 vertical（缩写 vert），来进行竖直分割。对于最常见的竖直分割操作，我们则可以直接写成 :vsplit（缩写 :vs）。

下面的动画展示了我们进行一次竖直分割后，再进行水平分割的过程：

窗口分割演示

多窗口编辑是一个比较适宜使用鼠标的情况。你可以使用鼠标来激活想要使用的窗口，也可以使用鼠标来拖拉窗口的大小——只要启用了鼠标支持，终端窗口（包括远程连接的 mintty、PuTTY 等）里的 Vim 的窗口分割线也是可以拖动的（上面动画里的分割线拖动就是在一个终端窗口里）。

当然，作为 Vim 用户，基本的键盘使用肯定是少不了的：

<C-W> 加方向键（h、j、k、l、<Left> 等等）可以在窗口之间跳转

<C-W>w 跳转到下一个（往右和往下）窗口，如果已经是右下角的窗口，则跳转到左上角的窗口

<C-W>W 跳转到上一个（往左和往上）窗口，如果已经是左上角的窗口，则跳转到右下角的窗口

<C-W>n 或 :new 打开一个新窗口

<C-W>c 或 :close 关闭当前窗口；当前窗口如果已经是最后一个则无效

<C-W>q 或 :quit 退出当前窗口，当最后一个窗口退出时则退出 Vim

<C-W>o 或 :only 只保留当前窗口，关闭其他所有窗口

<C-W>s 和 :split 作用相同，把当前窗口横向一分为二

<C-W>v 和 :vsplit 作用相同，把当前窗口纵向一分为二

<C-W>= 使得所有窗口大小相同（当调整过终端或图形界面 Vim 的窗口大小后特别有用）

<C-W>_ 设置窗口高度，命令前的数字表示高度行数，默认为纵向占满（想专心编辑某个文件时很有用）

<C-W>| 设置窗口宽度，命令前的数字表示宽度列数，默认为横向占满

<C-W>+ 增加窗口的高度，命令前的数字表示需要增加的行数，默认为 1

<C-W>- 减少窗口的高度，命令前的数字表示需要减少的行数，默认为 1

<C-W>> 增加窗口的宽度，命令前的数字表示需要增加的列数，默认为 1

<C-W><lt>（提醒，我们用 <lt> 表示“<”键）减少窗口的宽度，命令前的数字表示需要增加的列数，默认为 1

由于切换窗口是一个非常常见的操作，我通常会映射一下快捷键。为了跟一般的图形界面程序一致，我使用了 Ctrl-Tab 和 Ctrl-Shift-Tab：

nnoremap <C-Tab>   <C-W>w
inoremap <C-Tab>   <C-O><C-W>w
nnoremap <C-S-Tab> <C-W>W
inoremap <C-S-Tab> <C-O><C-W>W

简单解释一下：nnoremap 命令映射正常模式下的键盘，inoremap 命令映射插入模式下的键盘；正常模式的映射简单直白，应该不需要解释，插入模式的映射使用了临时模式切换键 <C-O>（:help i_CTRL-O），在正常模式下执行相应的窗口命令，然后返回插入模式。使用这样的键盘映射之后，这两个快捷键在正常模式和插入模式下就都可以使用了。

双窗口比较

多窗口编辑中有一个非常有用的使用方式，那就是比较两个文件，Vim 对此也有特殊的支持。使用 vimdiff 或 gvimdiff 命令，后面跟两个文件名，我们就可以对这两个文件进行比较。在比较时，Vim 会自动折叠相同的代码行，并加亮两边文本的不同部分。窗口的滚动也是联动的。一个实际的截图如下所示：

比较两个 C++ 文件

顺便说一句，因为使用双窗口比较功能要求 Vim 的宽度是平时的两倍左右，所以我通常都会对 Vim 窗口使用最大化、拖拉之类的操作。这些操作一般只影响右边的窗口的大小，因此，在放大窗口的操作后，我通常紧跟着就会执行 <C-W>= 来使两个窗口的宽度相同——事实上，我使用 <C-W>= 主要就在这种场合。你也可以试试。

当然了，在 Vim 内部也可以发起这样的比较。你需要做的是打开第一个文件，然后使用命令 :vert diffsplit 第二个文件。这一点只要了解一下就好，毕竟大部分情况下你不需要这样去做。

多标签页编辑

接下来我们继续讨论和多窗口编辑构成互补的另外一种方式，也就是多标签页。

这里我先给你一个结论：单窗口多文件编辑最适合的场景是批量修改具有相似性质的文件，多窗口编辑最适合的场景是需要对多个文件进行对比编辑，而其他的一些同时编辑多个文件的场景，就可以考虑多标签页的编辑方式。

我把多标签页编辑归为“其他”，但仍然还是有其特殊性质的。如果你熟悉现代多标签页的其他编辑器的话，你应该已经熟悉它的基本特性了。我们这儿再温习一下：

多标签页编辑允许在编辑器里同时修改多个（未存盘的）文件

多标签页编辑一次只展示一个文件

通过选择标签页（或使用键盘）可以方便地在多个标签页中进行切换

Vim 中的标签页在图形界面或终端模式下都能支持上面描述的这些特性。

终端里运行的有两个标签页的 Vim

鉴于你在实际使用中对标签页这种方式应当已经相当熟悉，如何使用标签页图形界面我就不讲了。有一点需要注意一下，和某些图形界面应用程序不同，Vim 里标签页可包含多个窗口（一个标签页里默认有一个窗口），而不是窗口可包含多个标签页——这也意味着，在标签页里关闭最后一个窗口就关闭了整个标签页。

此外，Vim 的标签页在纯文本的终端模式里也是可用的。在存在多个标签页的情况下，即使在终端里，你也可以用鼠标点击标签页来进行切换，双击标签栏的空白处添加新标签页，以及点击“X”标记来关闭标签页。

当然，Vim 用户更经常会使用键盘：

在已有命令行模式命令前加 tab␣ 可以在新标签页中展示命令的结果，如 :tab help 可以在新标签页中打开帮助，:tab split 可以在新标签页中打开当前缓冲区

:tabs 展示所有标签页的列表

:tabnew 或 :tabedit 可以打开一个空白的新标签页，后面有文件名的话则打开该文件

:tabclose 可以关闭当前标签页（如果标签页里只有一个窗口，使用窗口关闭命令 <C-W>c 应该更快）

:tabnext、gt 或 <C-PageDown> 可以切换到下一个标签页

:tabNext、:tabprevious 、gT 或 <C-PageUp> 可以切换到上一个标签页

:tabfirst 或 :tabrewind 切换到第一个标签页

:tablast 切换到最后一个标签页

这些命令跟多文件、多缓冲区的命令有诸多相似之处，我就不需要再多加描述了。

如果一开始用多窗口编辑，后来发现不需要一直参照这个文件了，或者屏幕空间不足了，该怎么办呢？Vim 提供了一个命令，可以把当前窗口转变成一个新标签页：按下 <C-W>T 即可（仅当当前屏幕上有多个窗口时有效）。

这讲我们对多窗口和多标签页编辑的基本讨论到这儿就暂告一个段落。不知道你记不记得，上一讲我们说过，如果某个 Vim 会话里不同的窗口（或标签页；以下略）编辑的是同一个文件，它们对应到的也会是同一个缓冲区。这意味着多个窗口编辑同一个文件不会有冲突，同时，如果缓冲区被修改了，但只要当前关闭的窗口不是包含这个缓冲区的唯一窗口，那关闭窗口不会有任何问题，也不会影响文件的状态。在任何一个瞬间，任何一个窗口都指向一个缓冲区，而任何一个缓冲区都属于一个或多个窗口。（例外情况是你使用了一个不那么常用的功能，隐藏缓冲区；这个功能在本课程中不会讨论。）

NERDTree 插件

讨论了多窗口和多标签页之后，我们来看几个利用这些特性的插件。我们讨论的第一个插件就是 NERDTree。

我们上一讲开始提出的问题就是对多个文件进行编辑。对于找文件这件事，NERDTree 就是你知道文件大概在哪里、但不知道文件具体名字时的一个好选择。跟很多 Vim 插件一样，NERDTree 会利用多窗口（少数情况下利用标签页）的特性。

拿我们上一讲提到的在文件开头插入版权声明的例子来说，至少在文件在一个目录下的情况下，使用一个文件浏览插件也能解决问题。NERDTree 就是最为著名的一个文件浏览 / 管理插件。下面是一个功能展示截图，先给你一个直观的印象：

NERDTree 的使用界面

安装

如果使用 minpac 的话，我们需要在 vimrc 中“Other plugins”那行下面加入下面的语句，并运行 :PackUpdate 来安装一下：

call minpac#add('preservim/nerdtree')

NERDTree 缺省就会抢占 netrw 使用的路径形式，所以我们可以用 :e . 来打开 NERDTree。不过，更常用的方式仍然是使用 :NERDTreeToggle，NERDTree 窗口的切换命令。我们使用这个命令可以打开上面左侧的那个 NERDTree 窗口，也可以关闭。这样，我们如果频繁需要浏览文件系统的话，就可以把这个命令映射到一个快捷键，免得每次都要打这么长的命令。鉴于功能键只有 12 个，映射其他键容易忘记，我暂时就不帮你在 vimrc 中映射了。对我来说，这个插件是需要安装的，但使用并不那么高频。

使用

在打开 NERDTree 窗口之后，使用还是相当直观的，并且按下 ? 就可以查看帮助信息，所以我也不必一一列举所有功能了。在这里，我就概要提一下最重要的几个功能点：

顾名思义，这个插件以树形方式展示文件系统，在目录上敲回车或双击即可打开或关闭光标下的目录树。

在文件上敲回车或双击立即打开该文件，并且光标跳转到文件窗口中，这样你就可以立即开始编辑了。

在文件上使用 go 会预览该文件，也就是光标不会跳转到文件所在的窗口中，方便快速查看多个文件的内容。

按 i 会打开文件到一个新的水平分割的窗口中，按 s 会打开文件到一个新的竖直分割的窗口中，按 t 会打开文件到一个新的标签页中。

NERDTree 会自动过滤隐藏文件和目录，但如果你需要看到它们的话，也可以用 I 来开启和关闭隐藏文件的显示。

按 m 会出现一个菜单，允许添加、删除、更名等操作。

这些命令不需要死记硬背。从使用的角度，知道回车、双击就可以使用这个插件了，其他命令可以根据需要，在使用中慢慢掌握。

类似插件

Vim 里预置了 netrw 插件，其功能包含多个网络文件协议，同时也包含了对本地文件系统的支持，使用 :e . 或 :vs . 这样的命令就可以直接启用。不过，它在本地目录浏览相关功能上比较简单，没有 NERDTree 好用。如果有条件安装 NERDTree 的话，你一定会更喜欢 NERDTree 的。

我的朋友明白（mbbill）写了个叫 VimExplorer 的插件，我也挺喜欢的，一直在用。它可以把 Vim 转变成一个双面板的资源管理器，设计上也更侧重于管理，而不是纯粹的文本编辑。不过，这个插件没有像 NERDTree 一样处于积极开发状态，我就不详细介绍了。有兴趣的可以自己试一下。

VimExplorer 的界面和右键菜单

内容小结

好了，这一讲到这里我就全部讲完了。我来简单做一下小结：

今天我们讲了 Vim 里的窗口概念和标签页概念，并讨论了相关命令。窗口的命令主要是以 <C-W> 开始的双键命令，而标签页的命令则和上一讲的文件操作命令和缓冲区操作命令非常相似，也用了“first”、“last”、“next”、“Next”、“previous”等英文单词，但前缀得使用“tab”。

多窗口适合你同时参照多个文件的内容，或者同一个文件的不同部分。多标签页适合其他你希望同时编辑多个文件的情况。在同一个 Vim 会话的多窗口和多标签页里编辑同一个文件不会发生任何冲突。

NERDTree 是一个利用多窗口和多标签页的流行插件，可以方便地在 Vim 里浏览文件系统并打开文件进行编辑。

本讲的配置文件更改也不多，对应的标签是 l6-unix 和 l6-windows。

课后练习

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

尝试在打开文件后使用 :split 和 :vsplit 分割窗口，改变窗口大小，在窗口里跳转，及关闭窗口。

使用 vimdiff 比较两个相似的文件。

运行 NERDTree，在多个标签页里打开当前目录下的文件。

尝试在标签页里切换，及关闭标签页。

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

07｜正则表达式：实现文件内容的搜索和替换

你好，我是吴咏炜。

上面两讲里我们讨论了如何找到你想要查看 / 编辑的文件，及如何处理多个文件。今天我们来看一下如何在一个文件中搜索和替换内容，其核心主题就是正则表达式。

正则表达式搜索

通过 Vim 教程，你已经学到了搜索命令 / 和替换命令 :s 的基本用法。教程里没有提到的是，你输入的待查找的内容是被 Vim 当成正则表达式来看待的。正则表达式的学习资料很多（极客时间上就有专门的课程），完整学习也相当复杂，我们就不从头学习了。下面我们会简单讨论的，是 Vim 里的正则表达式，重点是它和其他常用正则表达式（正则表达式还是有很多种不同的风格的）的区别之处。如果你之前对正则表达式完全没有了解，建议你这儿暂停一下，先在网上搜索一下关于正则表达式的资料，了解它的基本概念和用法，然后继续阅读。

在一个搜索表达式里，或者称为模式（pattern；注意不要和 Vim 的 mode 混淆）里，.、*、^、$、~、[]、\ 是有特殊含义的字符：

. 可以匹配除换行符外的任何字符：如 a. 可以匹配“aa”、“ab”、“ac”等，但不能匹配“a”、“b”或“ba”。如果需要匹配换行符（跨行匹配）的话，则需要使用 \_.。

* 表示之前的匹配原（最普通的情况为单个字符）重复零次或多次：如 aa* 可以匹配“a”、“aa”或“aaa”，a.* 可以匹配“a”、“aa”、“abc”等等，但两者均不能匹配“b”。

^ 匹配一行的开头，如果出现在模式的开头的话；在其他位置代表字符本身。

$ 匹配一行的结尾，如果出现在模式的结尾的话；在其他位置代表字符本身。

~ 匹配上一次替换的字符串，即如果上一次你把“foo”替换成了“bar”，那 ~ 就匹配“bar”。

[…] 匹配方括号内的任一字符；方括号内如果第一个字符是 ^，表示对结果取反；除开头之外的 - 表示范围：如 [A-Za-z] 表示任意一个拉丁字母，[^-+*/] 表示除了“+”、“-”、“*”、“/”外的任意字符。

\ 的含义取决于下一个字符，在大部分的情况下，包括上面的这几个（.、*、\、^、$、~、[ 和 ]），代表后面这个字符本身；在跟某些字符时则有特殊含义（后面我们会讨论最重要的那些）。

除此之外的字符都是普通字符，没有特殊含义。不过，需要注意的是，如果使用 / 开始一个搜索命令，或者在替换命令（:s）中使用 / 作为模式的分隔符，那模式中的 / 必须写作 \/ 才行，否则 Vim 看到 / 就会以为模式结束了，导致错误发生。

为了避免写模式的困扰，如果模式中使用“/”作为路径的分隔符，在替换命令中可以使用其他模式中没有的符号作为分隔符。比如，想把“/image/”全部替换成“/images/”的话，不要用 :%s/\/image\//\/images\//g，而应该用类似于 :%s!/image/!/images/!g 的写法。这只能适用于替换命令，而在使用 / 命令搜索时我们就没什么好办法了，只能把模式里的 / 写作 \/。不过我们也可以取巧一下，用 ? 向上、也就是反向搜索，只要记得 n、N 反过来用找下一个就行。

通过 \ 开始的特殊表达式有不少，如果你需要完整了解的话，可以去看看参考文档（:help pattern-overview）。我们下面先学习一下最基本的 6 个特殊模式项：

\? 表示之前的匹配原重复零次或一次：如 aa\? 可以匹配“a”、“aa”，但不能完整匹配“aaa”（可以匹配其前两个字符、后两个或最后一个字符）。

\+ 表示之前的匹配原重复一次或多次：如 aa\+ 可以匹配“aa”、“aaa”，但不能匹配“a”或“b”。

\{n,m} 表示之前的匹配原重复 n 到 m 遍之间，两个数字可以省略部分或全部：如 a\{3}（可读作：3 个“a”）可以匹配“aaa” ，a\{,3}（可读作：最多 3 个“a”）可以匹配“”、“a”、“aa”和“aaa”；两个数字都省略时等价于 *，也就是之前的匹配原可以重复零次或多次。

\( 和 \) 括起一个模式，将其组成为单个匹配原：如 \(foo\)\? 可以表示单词“foo”出现零次或一次。\( 和 \) 还有一个附加作用，是捕获匹配的内容，按 \( 出现的先后顺序，可以用 \1、\2 到 \9 来引用。如果你不需要捕获匹配内容的话，用 \%( 和 \) 的性能更高。

\& 是分支内多个邻接（concat）的分隔符，概念上可以和与操作相比，表示每一项都需要匹配成功，然后取最后一项的结果返回：如 .*foo.*\&.*bar.* 匹配同时出现了“foo”和“bar”的完整行。相对来讲，\& 没那么常用。

\| 是多个分支的分隔符，概念上可以和或操作相比，表示任意一项匹配成功即可：如 foo\|bar 可匹配“foo”或“bar”两单词之一。

接下来，我再和你分享 13 个特殊模式项。虽然它们相对来说不那么必需，但掌握它们可以大大地提高程序员的编辑效率。

\< 匹配单词的开头

\> 匹配单词的结尾

\s 匹配空白字符 <Space> 和 <Tab>

\S 匹配非空白字符

\d 匹配数字，相当于 [0-9]

\D 匹配非数字，相当于 [^0-9]

\x 匹配十六进制数字，相当于 [0-9A-Fa-f]

\X 匹配非十六进制数字，相当于 [^0-9A-Fa-f]

\w 匹配单词字符，相当于 [0-9A-Za-z_]

\W 匹配非单词字符，相当于 [^0-9A-Za-z_]

\h 匹配单词首字符，相当于 [A-Za-z_]

\H 匹配非单词首字符，相当于 ^[A-Za-z_]

\c 忽略大小写进行匹配

以上我们讨论的实际上是 Vim 缺省设置下的正则表达式。通过选项（:help /magic），我们可以对哪些字符有特殊意义进行一定程度的调整。不过一般情况下，我认为修改这个选项只会造成混乱、增加心智负担，因此我也就不在这儿展开了。

搜索实例

抽象地讨论正则表达式恐怕你也不容易记住，我们还是拿一些具体的例子来看一下吧。

首先，如果我们要查找某个函数，该怎么做呢？简单，按下 /，然后输入函数名，回车，不就行了？

错。这种方式对函数名是部分匹配，你搜 begin 还会得到 begin1、_begin 之类的结果。正确的方法是，要在前后加上匹配单词头尾的标记，如，\<begin\>。

顺便说一句，被誉为最有用的 Vim 提示，是把光标移到希望搜索的关键字上，然后按下 * 键。Vim 会提取光标下的关键字，并自动添加 \< 和 \> 进行搜索。

使用 * 搜索的示例；注意“unexpected”并没有被高亮

如果我要搜索 begin 或 end 呢？我想，你应该已经知道了，是：/\<\(begin\|end\)\>。注意，写成 /\<begin\|end\> 可是不对的。（为什么？你想明白了吗？）

对于 HTML，你应该多多少少有些了解。如果我们想匹配一下 HTML 标签的话，该怎么做呢？

一个标签以 < 开始，以 > 结束。所以，最简单的模式应该是 <.\+>，对吗？

不对，这个写法忽略了一行里可能有多个标签的事实：对于“<h1>title</h1>”这样一个字符串，上面这个简单的模式会匹配整个字符串，而不是“<h1>”和“</h1>”……

HTML 标签的错误匹配

有一种解决方案是，排除不应该匹配的字符，把模式写成 <[^>]\+>：一对尖括号里有一个或多个不是“>”的字符。不过，这样的写法会让像 > 这样的结尾字符在模式中重复出现，因此这并不是最理想的写法。更好的方式是，使用最短匹配。

最长匹配和最短匹配

我们上面学到的 *、\?、\+ 和 \{} 都属于最长匹配（也叫贪婪匹配），也就是说，当模式既可以匹配一个较长的字符串，也可以匹配一个较短的字符串时，结果会是那个较长的字符串。

相应地，还有一种匹配叫做最短匹配，也就是在同时可以匹配较长的字符串和较短的字符串时，产生较短的匹配。在 Vim 里，最短匹配只有一种形式，{-n,m}，其意义和之前说的 {n,m} 基本相同，但结果是较短而非较长的字符串。

以上面的 HTML 标签匹配为例，使用最短匹配的话，我们可以把模式写成 <.\{-1,}>，要求在一对尖括号里至少有一个字符，但越短越好。

搜索加亮和取消

如果你一边学一边在试验的话，就会发现，Vim 缺省在你输入搜索模式时就会高亮跟你输入的模式匹配的文本。这对验证你输入的模式是否正确，以及进行进一步的编辑，都是非常方便和重要的。用惯了 Vim，就会把它当成是一件理所当然的事——直到你被迫使用其他编辑器时才发现，一边输入正则表达式一边就能看到匹配的结果，原来不是谁都这样做的啊……

但也有些时候，我们已经做完了搜索或替换，和模式匹配的文本内容仍然还高亮着，非常碍眼。有些人就会随便搜索一个不存在的字符串来取消加亮，但这显然不是一种高效的处理方式。事实上，Vim 有一个专门命令来取消搜索加亮，这个命令就是 :nohlsearch，不要高亮搜索。

鉴于这个命令使用的频度实在是太高了，我们需要给它专门分配一个快捷键。请在 vimrc 中加入：

" 停止搜索高亮的键映射
nnoremap <silent> <F2>      :nohlsearch<CR>
inoremap <silent> <F2> <C-O>:nohlsearch<CR>

这样一来，在搜索或替换工作完成之后，只要按下 <F2> 就可以取消搜索加亮了。

好，关于正则表达式的搜索部分，我们暂时就先学到这里。下面我们来看一下替换。

正则表达式替换

你可能要说了：替换不就是找到跟模式匹配的字符串，然后把它换成另外一个字符串么，有什么复杂的？

事实上，还真是有些复杂情况的。你在看下面这些复杂的替换情况时，也可以同时考虑下自己有没有解决方案：

你可能要保留匹配中的某些字符，而替换另外一些字符

你可能要对匹配出的内容做大小写转换

你可能需要“计算”出替换结果

你可能需要决定一行里要替换单次还是多次，是自动替换还是要一一确认，等等

接下来，我们就分别看看这些复杂情况。

在这些情况里，最常用的显然就是在替换结果中保留匹配出的字符串了。前面说到 \(\) 除了将一个模式转变成匹配原外，还有一个作用是捕捉匹配的内容，按 \( 的出现顺序依次编号为 1 到 9，并可以在模式和替换字符串中用 \1 到 \9 来访问。如果要在替换字符串中完整使用匹配内容的话，则可以使用 \0 或 &（字符“&”也因此要在替换字符串中写成 \&）。

从搜索的角度，我们一般只关心匹配与否，而不关心匹配的大小。举个例子，如果我想找出作为函数调用的 begin，那我可以写成 \<begin(，虽然 ( 不是我想匹配的内容（函数名称）的一部分。但从替换的角度，我需要在替换时再处理一下多匹配的内容，也是件麻烦事；在非匹配的内容比较复杂或者会变化的时候，尤其会是这样。所以 Vim 里还有专门标识匹配开始和结束的匹配原，分别是 \zs 和 \ze。对于这个例子，搜索模式就应该是 \<begin\ze(。为了巩固前面学到的知识，你应该知道，这个模式也可以啰嗦地写成 \<begin(\&begin 或 \<begin(\&.....。

Vim 里还有一些大小写转换的特殊替换字符串。它们是：

\U 把下面的字符变成大写，直到 \E 出现

\u 把下一个字符变成大写

\L 把下面的字符变成小写，直到 \E 出现

\l 把下一个字符变成小写

\E 结束大小写转换

Vim 还能用 \= 开始一个返回字符串的表达式，用来计算出一个替换结果。鉴于我们目前还没有讨论 Vim 脚本，这个我们就留到后面第 14 讲再说了。

跟常用的编程语言一样，Vim 的正则表达式中支持 \t、\r、\n 等特殊转义字符，但在替换表达式中，由于一些技术原因（:help NL-used-for-Nul），\n 插入的是空字符（NUL 或“\0”），而非在模式中出现时代表的 LF。如果要插入正常的行尾符 LF 的话，我们得使用 \r。这意味着如果想把一个回车变成两个的话，我们得别扭地写 :s/\n/\r\r/，略遗憾。如果有特殊需要得插入 CR 的话，就要更别扭地输入 \<C-V><CR> 才行。还好，我们基本不会在替换时遇到要插入 CR 的情况……

Vim 有很多用来控制替换的标志，你可以通过 :help s_flags 查看详细的介绍，我就不一一列举了。今天这一讲中，我们只会用到最常用的一个标志，g，代表可以在一行内进行多次替换；没有这个标志的话，Vim 在一行里只会对第一个成功的匹配进行替换。

替换实例

同样，我们还是通过例子来巩固一下对正则表达式替换的理解。

先来看一个简单的，删除行尾的“//”注释。我们可以用这个命令

:%s!\s*//.*$!!

把零到多个空白字符后面出现的“//”直到行尾全部删除。

如果要删除“/* */”注释，那就复杂多了。首先，匹配内容可以跨行；其次，有跟 HTML 标签类似的问题，需要使用最短匹配。我们需要使用的命令是：

:%s!/\*\_.\{-}\*/!!g

由于一行里可以有多个“/* */”注释，我们在替换命令的尾部还加上了 g 标志，允许一行里进行多次替换。

假设我们目前的编码规范规定，所有的函数名应该首字母大写（简单起见，我们假设所有的类名已经是首字母大写了，因而构造函数自动符合该要求，不会发生冲突；但其他很多函数名称仍然是小写字母开头），我们能不能用 Vim 的替换命令做到呢？答案也是肯定的。所有需要的知识点我们都已经讲过了，我就直接公布答案了：

:%s/\<\(_*\)\([a-z]\w*\)\ze(/\1\u\2/g

这个命令比较长，请你慢慢体会一下，尝试去理解每一部分的意图。如果你有哪个点卡住了，可以留言给我，我再帮你详细分析一下。

内容小结

好了，今天的内容就讲到这里了。内容有点密集，我把要点再总结一下：

Vim 支持用 / 进行搜索和用 :s 进行替换，它们都用到了正则表达式。

在搜索的模式里，.、*、^、$、~、[]、\ 是有特殊含义的字符，你一定要记住它们的含义。在 \ 开始的特殊表达式中，最重要的是 \?、\+、\(\)、\| 和 \{n,m}。对于程序员来说，\<、\> 等匹配原对于提高编辑效率也非常重要。Vim 中的常用搜索命令 * 则会自动在搜索的关键字前后加上 \< 和 \>。

在替换时，我们需要特别记住 \1、\2 到 \9 可以用来引用前面用 \( 和 \) 括起来的内容，字符“&”出现在替换内容中需要使用反斜杠转义成 \&，否则代表完整的被匹配字符串。

正则表达式就可以算是一种独立的语言了，靠死记硬背是不行的。最后我还要建议你再把这一讲中的例子仔细看一下、尝试一下，多多练习是掌握正则表达式搜索和替换的必经之路。如果你日后遇到了这一讲没有覆盖的问题，可以再去查阅 Vim 的帮助文档 :help regexp。

本讲我们在配置文件中只更改了一处，对应的标签是 l7-unix 和 l7-windows。

课后练习

有两道练习题，请你在课后尝试一下。练习对于掌握正则表达式是非常重要的。

如果我要搜索“/* */”注释的话，搜索命令应该是什么样的？

例子里只说了首字母大写，但实际的编码规范是要求把 begin_search_nocase 这样的函数名称转变成 BeginSearchNocase。请用 Vim 的替换命令完成这一任务。提示：可能需要一条以上的替换命令。

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

08｜基本编程支持：规避、解决编程时的常见问题

你好，我是吴咏炜。

在前面的几讲里，我们已经学了很多使用 Vim 的基本知识。今天是编程专题，我来专门介绍一下 Vim 对编程的特别支持。学了这一讲之后，你会进一步了解 Vim 编辑程序时的重要特性，并能够规避、解决编程时的常见问题。

文件类型和关联设定

程序源代码通常由文件组成，每个文件都有一个关联的文件类型。这个文件类型决定了 Vim 对其进行处理的一些基本设定，可能包括：

如何对文件进行高亮

制表符（tab）的宽度（空格数）

是否在键入 <Tab> 时扩展为空格字符

每次缩进的空格数（是的，可以和制表符宽度不同）

采用何种自动缩进方法

其他可适用的选项

文件高亮通常需要一套相当复杂的规则，我们今天就只把它当成一个既成事实了，不讨论这些规则的细节。其他各项在 Vim 里一般以选项的形式出现。这些选项都是文件本地（local）选项，即可以在一个文件里修改其数值而不影响其他文件。对于这样的选项，可以用 :setlocal 和 :setglobal 命令分别访问本地值和全局值。一般的 :set 命令在读取数值时（如 :set tabstop?）返回本地值，在写入数值时（如 :set tabstop=4）同时设置本地值和全局值。

制表符宽度对应的选项是 tabstop。这在不同的语言里可能有不同的惯例，自然不必多说。它的缺省值是 8，但在不同的文件里可以不一样。不同的文件类型也可能会自动设定不同的数值。

是否扩展 <Tab> 为空格由 expandtab 选项控制。我们前面看到过，但没有讲过，Vim 选项有些是用等号赋值的，也有些不用等号，而只用选项名称或选项名称前面加 no，表示否定。这些就是布尔类型选项，expandtab 也是其中之一。如果打开了 expandtab 选项，那输入中的 tab 会被转变成空格；如果关闭的话，则 tab 字符会被保留。

让事情变得更复杂的是，Vim 还有个 softtabstop 选项，软制表符宽度。一旦设置了这个选项为非零值，再键入 <Tab> 和 <BS>（退格键），你就感觉像设置了这个宽度的 tabstop 一样，有相应数量的缩进或取消缩进，但实际插入的字符仍然受 expandtab 和 tabstop 两个选项控制。在设置软制表符宽度时，一种最常用的用法是同时设置 expandtab，这样，编辑时你感觉像使用了这个宽度的制表符一样，但你输入的内容里实际被保存的仍然是空格字符。

这些还不是 Vim 真正使用的“缩进”值。以 C 语言为例，当 Vim 看到你输入“{”和回车键时，会自动产生一个缩进，而这个缩进值跟 tabstop 和 softtabstop 都无关，是一个独立的选项 shiftwidth。

最后，Vim 还有很多精细的选项来控制如何进行缩进。默认情况下，Vim 没有特殊缩进，回车键回到行首。一般而言，使用选项 autoindent 可以使 Vim 至少记住上一行的缩进位置；而对于特定语言，Vim 可以设置更合适的选项，达到更佳的缩进效果——如对类 C 语言 Vim 会设置 cindent 选项，达到最优的缩进效果。我们下面还会提到，Vim 支持对类 C 语言的缩进有一些精调选项，你也可以自己进一步进行调整。

我之前提到过，Vim 会根据文件类型来设置选项。所以，相关问题就是，Vim 如何判断文件类型，如何根据文件类型来设置选项，以及我们该如何定制这些行为。我们下面就来一一作答。

文件类型判断

Vim 的文件类型判断是在 filetype.vim 中执行的。我们可以用下面的命令来打开这个文件：

:e $VIMRUNTIME/filetype.vim

这个文件相当复杂，但有编程功底的你，应该可以看出大概的意思吧？其中最主要的逻辑仍然是通过后缀来进行判断，如：

" C++
au BufNewFile,BufRead *.cxx,*.c++,*.hh,*.hxx,*.hpp,*.ipp,*.moc,*.tcc,*.inl setf cpp

其中 au 是 autocmd 的缩写，代表 Vim 在发生某事件时触发某一动作。上面说的就是在创建（BufNewFile）或读入（BufRead）跟指定文件名模式匹配的文件时，把文件类型设为 C++（setf cpp， setf 是 setfiletype 的缩写）。

但在后缀不足以唯一判断时，Vim 可以进一步执行代码，如：

au BufNewFile,BufRead *.h      call dist#ft#FTheader()

上面函数的定义在文件 $VIMRUNTIME/autoload/dist/ft.vim 里：

func dist#ft#FTheader()
  if match(getline(1, min([line("$"), 200])), '^@\(interface\|end\|class\)') > -1
    if exists("g:c_syntax_for_h")
      setf objc
    else
      setf objcpp
    endif
  elseif exists("g:c_syntax_for_h")
    setf c
  elseif exists("g:ch_syntax_for_h")
    setf ch
  else
    setf cpp
  endif
endfunc

它的大概意思是，如果在头 200 行里找到某行以 @interface 等内容开始，那就认为这是 Objective-C/C++，否则认为是 C/C++。具体是 C 还是 C++，则由全局变量 g:c_syntax_for_h 控制（我们忽略 Ch 这种小众情况）。详细语法我们就不展开讲述了，留待讨论 Vim 脚本的时候再看。

上面讲的是 Vim 的缺省行为。我们当然也可以定制 Vim 的行为。按照惯例，一般把定制放在用户 Vim 配置目录里的 filetype.vim 里。我的定制如下所示：

if exists("did_load_filetypes")
  finish
endif

function! s:CheckCPP()
  if expand('%:t') !~ '\.'
    setfiletype cpp
  endif
endfunction

augroup filetypedetect
  au! BufRead,BufNewFile *.asm      setfiletype masm
  au! BufRead proxy.pac             setfiletype javascript
  au! BufRead */c++/*               call s:CheckCPP()
  au! BufRead */include/*           call s:CheckCPP()
augroup END

我们可以跳过一些语法方面的细节，只讨论代码里的意图。上面这段代码主要做了以下事情：

当读入或创建后缀为“.asm”的文件时，设置文件类型为微软宏汇编（默认为 GNU 的汇编格式）。

当读入名字为“proxy.pac”的文件时，把内容当成 JavaScript 解释。

当读入路径含“c++”或“include”的文件时，调用脚本内部函数 CheckCPP，检查文件名（% 代表文件名，:t 代表尾部，即去掉路径部分）是否不含“.”，是的话当成 C++ 文件类型。这是为了处理像“memory”这样的无后缀 C++ 头文件。

随后 Vim 会继续载入自带的 filetype.vim；如果文件类型还未确定的话，则继续使用 Vim 自带的规则进行判断。

文件类型选项

一旦确定了文件类型，Vim 会从运行支持文件目录下载入同名的文件。以 Python 为例：

syntax/python.vim 包含了如何对 Python 进行语法加亮的设置

indent/python.vim 包含了如何对 Python 代码进行缩进的设置（如在用户输入 if 时进行缩进等）

ftplugin/python.vim 是文件类型插件，包含了其他跟文件类型相关的设置

文件类型插件中包含我们上面提到的制表符宽度方面的设定，具体来说，是下面这几行：

if !exists("g:python_recommended_style") || g:python_recommended_style != 0
    " As suggested by PEP8.
    setlocal expandtab shiftwidth=4 softtabstop=4 tabstop=8
endif

默认情况下，该文件使用 PEP 8 推荐的设置：

把用户输入的制表符扩展成空格

缩进和软制表符宽度设为 4

如果文件中包含制表符的话，仍按宽度为 8 来解释

缩进和软制表符宽度设成 4 估计不需要解释，这应该是最常用的缩进值了。使用空格而不是制表符的最大好处是，在无论何种环境下，展示效果都可以完全一致，不会在 diff 时或制表符宽度不符合预期时代码就乱了。至于“硬”制表符宽度仍然是 8，则是为了确保显示文件的兼容性，尤其是在终端里 cat 文件时和在浏览器中浏览源代码时；这两种情况下，制表符宽度一般都是 8。

跟 Python 不同，很多其他文件类型没有推荐的风格设定，这时就应该用户自己进行设定了。我推荐在 vimrc 配置文件里进行设置，因为比较集中、容易管理。如：

au FileType c,cpp,objc  setlocal expandtab shiftwidth=4 softtabstop=4 tabstop=4 cinoptions=:0,g0,(0,w1
au FileType json        setlocal expandtab shiftwidth=2 softtabstop=2
au FileType vim         setlocal expandtab shiftwidth=2 softtabstop=2

上面设置了几种不同文件类型的编辑选项。大部分我们都已经知道了，下面这个则是新的：

cinoptions 可以精调 C 风格缩进的方式；上面 :0 表示 switch 下面的 case 语句不进行额外缩进，g0 代表作用域声明（public:、private: 等）不额外缩进，(0 和 w1 配合代表没结束的圆括号里的内容折行时不额外缩进。

我们也可以根据文件类型以外的条件来进行设定，如下面设定是要把 /usr/include 目录下的文件按 GNU 编码风格来解释：

function! GnuIndent()
  setlocal cinoptions=>4,n-2,{2,^-2,:2,=2,g0,h2,p5,t0,+2,(0,u0,w1,m1
  setlocal shiftwidth=2
  setlocal tabstop=8
endfunction

au BufRead /usr/include/*  call GnuIndent()

GNU 风格的 C++ 代码（注意大括号的缩进风格和高亮的 tab 宽度）

当然，除了设定选项，我们也可以做其他事情，比如下面的代码是在 Vim 帮助文件中，将 q 设定为关闭窗口的按键，映射中的 <buffer> 表示该映射只对这个缓冲区有效。

au FileType help  nnoremap <buffer> q <C-W>c

Tags 支持

Vim 对一种叫 tags 的文本索引格式有特殊支持。事实上，Vim 自己的帮助文件都是用 tags 来索引的。我们用过了 Vim 帮助，也就用过了 tags 文件。下面展示了 $VIMRUNTIME/doc/tags 文件中的一部分：

?       pattern.txt     /*?*
?<CR>   pattern.txt     /*?<CR>*
@       repeat.txt      /*@*
@/      change.txt      /*@\/*
@:      repeat.txt      /*@:*
@=      change.txt      /*@=*
@@      repeat.txt      /*@@*
@r      eval.txt        /*@r*
A       insert.txt      /*A*
ACL     editing.txt     /*ACL*
ANSI-C  develop.txt     /*ANSI-C*

我们可以清楚地看到，其中内容分为三列：第一列是关键字，第二列是文件名，第三列是在目标文件中的匹配文本。当你在 Vim 的帮助文件中使用双击或 <C-]> 等命令跳转时，Vim 就会在 tags 文件中搜索，寻找到匹配项的时候就跳转到指定的文件，并利用匹配文本跳转到指定的位置。

注意我们有不止一个 tags 文件。单单从 Vim 帮助的角度，个人 Vim 配置目录下的 doc 目录里有一个 tags 文件；每当你装了一个新的带帮助文件的 Vim 插件时，你都需要到这个 doc 目录下运行 helptags . 来重新生成索引。每个 Vim 软件包的 doc 目录下也同样需要有 tags 文件，不过包管理器能够在安装、更新时自动帮我们在 doc 目录下生成 tags 文件。Vim 在你使用 :help 命令查帮助时，会自动在你的所有运行时目录（可以使用 :set runtimepath? 查看）下的 doc/tags 里查找第一个匹配项。

生成 tags 文件的工具

如果 tags 文件只支持 Vim 帮助文件的话，那我就没必要对其进行详细讨论了。之所以在这里讨论 tags，是因为它可以用在编程语言上。要生成 Vim 可以使用的支持常用编程语言的 tags 文件，我们需要使用下列两个工具之一：

Exubertant Ctags

Universal Ctags

Exuberant Ctags 是已经存在了好多年的老牌工具。Windows 下可直接下载可执行程序，而 Linux 和 macOS 上的包管理器一般也直接支持。如：

Ubuntu 下可使用 sudo apt install exuberant-ctags

CentOS 下可使用 sudo yum install ctags

macOS Homebrew 可使用 brew install ctags（但需要注意 macOS 本身提供了一个功能较简单的 ctags 命令，你可能需要将 /usr/local/bin 在路径里移到 /usr/bin 前面，或自己设置 alias，确保优先使用 /usr/local/bin/ctags）

Universal Ctags 还比较新，目前各操作系统的包管理器里多半还没有它，所以安装会麻烦一点。你需要自己查看文档，找到在你的操作系统上的安装方式。（我是直接从源代码编译了一个版本。）

我之所以要推荐 Universal Ctags，是因为虽然 Exuberant Ctags 和 Universal Ctags 都支持超过 40 种的常见编程语言，但 Exuberant Ctags 的最后一个版本 5.8，发布于 2009 年，之后就一直没有更新了。Universal Ctags 是基于 Exuberant Ctags 代码的改进版本，并把开发移到了 GitHub 上，项目一直处于活跃状态。想偷懒的话，可以直接使用 Exuberant Ctags；如果愿意折腾一下，或者明确遇到 Exuberant Ctags 的问题，则可以试试 Universal Ctags。

对于现代 C++ 代码，使用 Universal Ctags 还是挺重要的。老的 Exuberant Ctags 不能处理 C++11 以来的新语法——这当然也是件显而易见的事。

生成 tags 文件的命令

要生成 tags 文件时，你可以简单地进入到一个目录下，然后执行下面的语句对该目录及子目录下的程序源文件生成一个 tags 文件：

ctags -R .

但根据场景和语言不同，你可能需要使用更多的选项。比如，对于 C++，我一般使用：

ctags --fields=+iaS --extra=+q -R .

如果是对系统的头文件生成 tags 文件——可以用来查找函数的原型信息——那我们一般还需要加上 --c-kinds=+p 选项。为了一次性地对系统头文件简单地生成 tags 文件，我还专门写了个脚本 gen_systags 来自动化这项工作。你如果感兴趣的话，也可以点进去看一下。

鉴于我们主要讲 Vim 而不是 Ctags，这个话题我暂时就点到为止、不展开了。你可以通过我给出的链接，以及 man ctags 或 ctags --help 的输出，自己进一步学习一下。讲到 C 的工作环境时，我们会再回到 Ctags。

使用 tags 文件

如果当前目录下或当前文件所在目录下存在 tags 文件，Vim 会自动使用这个文件，不需要你做额外的设定。你所需要做的就是在待搜索的关键字上（也可以在可视模式下选中需要的关键字）使用正常模式命令 <C-]>，或者按 g（g 可理解成 go）键加鼠标单击。你愿意的话，也可以手工输入命令 :tag 后面跟空格和待搜索的符号加回车键。这样 Vim 即会跳转到该符号的定义或声明位置。

标签跳转示例

如果待搜索的符号找不到，Vim 会报错“E426: tag not found”。如果存在一个或多个匹配项，Vim 会跳转到第一个匹配的位置。下面我列举一下其他相关的常用命令：

:tnext（缩写 :tn）跳转到下一个标签匹配位置

:tNext（缩写 :tN）或 :tprevious（缩写 :tp）跳转到上一个标签匹配位置

:tfirst 或 :trewind 跳转到第一个标签匹配位置

:tlast 跳转到最后一个标签匹配位置

:tselect 名称（:tselect 可缩写为 :ts）跟 :tag 类似，但会列举可能的匹配项，让你自己选择（而非跳转到第一个匹配位置）

g] 跟 <C-]> 类似，但跟 :tselect 一样会给出一个列表而非直接跳转

:tjump 名称（:tjump 可缩写为 :tj）跟 :tselect 类似，但在只有一个匹配项的时候会直接跳转到匹配位置

g<C-]> 跟 g] 类似，但跟 :tjump 一样在只有一个匹配项时会直接跳转到匹配位置

:stselect 名称（:stselect 可缩写为 :sts）跟 :tselect 类似，但结果会打开到一个新分割的窗口中

:stjump 名称（:stjump 可缩写为 :stj）跟 :tjump 类似，但结果会打开到一个新分割的窗口中

我们的标签跳转分为 :tag、:tselect 和 :tjump 三种不同方法，正常模式和可视模式的命令 <C-] 也同样有后两种方法的变体，对应的命令分别是 g] 和 g<C-]>。这三个命令前面也都可以额外加上 <C-W>，表示结果打开到新窗口中而非当前窗口。

展示在新窗口中选择并打开 printf 的声明

Vim 默认只在当前目录下和文件所在目录下寻找 tags 文件。对于含多层目录的项目，这个设定就不合适了。解决方法是使用 Vim 的选项 tags。一个小技巧是根据项目的可能深度，检查上层存在的 tags 文件：

" 加入记录系统头文件的标签文件和上层的 tags 文件
set tags=./tags,../tags,../../tags,tags,/usr/local/etc/systags

tags 选项的默认值是 ./tags,tags，即检查文件所在目录下的 tags 文件和当前目录下的 tags 文件。上面这样的写法还会额外检查父目录下的 tags 文件，祖父目录下的 tags 文件，以及我们上面用 gen_systags 生成的 systags 文件。这对一个有不超过三层目录结构的项目来讲就足够了。如果你的项目目录层次更深，也只需要在 tags 选项里添加 ../../../tags 这样的内容即可。

Tagbar 插件

根据上面的描述，我们可以看到 Ctags 是一个可以从源代码中提取符号的工具。事实上，这个工具在我们不生成 tags 文件也都是有用的。Vim 的插件 tagbar 就可以利用 Ctags 来提取符号，生成源代码的结构图。只要 Ctags 能支持这种语言，插件就能“识别” 这种语言，来生成结构图；识别的好坏程度也视 Ctags 对其的支持程度而定。下面是一个示例：

Tagbar 示例

跟之前类似，假设使用 minpac 的话，我们需要在 vimrc 中“Other plugins”那行下面加入下面的语句，并运行 :PackUpdate 来安装一下：

call minpac#add('majutsushi/tagbar')

我给它映射了快捷键 <F9>，可以快速打开和关闭 Tagbar 的窗口：

" 开关 Tagbar 插件的键映射
nnoremap <F9>      :TagbarToggle<CR>
inoremap <F9> <C-O>:TagbarToggle<CR>

Quickfix 窗口

Vim 里有一种特殊类型的窗口，被称作 quickfix（快速修复）。这个窗口中会展示外部命令的结果，并可以通过这个窗口中的内容直接跳转到特定文件的特定位置。这个设计最初是用来加速“编辑 - 编译 - 编辑”这个循环的，但它的实际用处并不只是用来编译程序。

我们先来看一下 Vim 的 :make 命令。如果你的代码可以简单执行 make 来编译的话（也就是说，你已经写了或者生成了合适的 Makefile），你可以尝试直接在 Vim 里执行 :make。你会看到正常的执行过程。唯一不一样的地方是，如果编译失败了，Vim 会自动跳转到第一个出错的位置！

演示 :make 失败时的行为

如果使用 :copen 命令，我们就可以打开 quickfix 窗口。在里面我们可以看到完整的出错信息，并能通过颜色看出 Vim 解析了文件名和行号。我们在带文件名的行上双击即可跳转到对应位置。另外，我们在 quickfix 窗口中也有跟之前类似的“next”类命令：

:cnext（缩写 :cn）跳转到下一个出错位置

:cNext（缩写 :cN）或 :cprevious（缩写 :cp）跳转到上一个出错位置

:cfirst 或 :crewind 跳转到第一个出错位置

:clast 跳转到最后一个出错位置

事实上，在这些下一个、上一个的命令中，我用得最多的就是这个快速修复里的跳转了。为了方便记忆，我对它们都映射了相似的快捷键。

" 用于 quickfix、标签和文件跳转的键映射
nmap <F11>   :cn<CR>
nmap <F12>   :cp<CR>
nmap <M-F11> :copen<CR>
nmap <M-F12> :cclose<CR>
nmap <C-F11> :tn<CR>
nmap <C-F12> :tp<CR>
nmap <S-F11> :n<CR>
nmap <S-F12> :prev<CR>

这是我的映射，你可以根据自己的需要进行调整。另外要留意的一点是，取决于环境，不是所有的快捷键都能被 Vim 接收到，尤其在使用终端和远程连接的时候。比如，在 Mac 上有些快捷键已经被系统占用，并且终端基本不接受修饰键；在 Windows 的远程连接客户端里，PuTTY 不支持使用 Alt 的快捷键，但 mintty 就可以。

:make 命令的其他细节

Vim 里的 :make 命令缺省会执行 make 命令，并且这是可以通过选项 makeprg 来进行配置的。比如，如果你希望启用四路并发编译，你就可以设置 :set makeprg=make\ -j4。你也可以使用 GNU Make 之外的构建工具，但需要注意的是，如果发现 Vim 不能识别你使用的构建工具产生的错误信息，你可能需要利用 errorformat（:help errorformat）选项来告诉 Vim 如何处理错误信息。

:grep 命令

对我而言，跟构建使用频度至少一样高的命令是搜索，也就是根据关键字找到相关的源代码。这就可以使用 Vim 的 :grep 命令。跟 :make 命令相似，Vim 会调用一个合适的外部程序（可通过 grepprg 选项来进行配置）来进行搜索，并从结果中找到文件名、行号等信息。注意：在 Windows 上如果 Vim 没找到 grep 的话，它会调用 Windows 自带的 findstr 命令行工具；为了获得跟其他平台相同的体验和跟 Vim 本身相似的正则表达式，我强烈推荐你在 Windows 上也安装 grep 工具。我们上一讲讲到的搜索模式，大部分在 grep 里可以原封不动地使用，尤其是对 \?、\+、\< 和 \> 的解释。考虑到 vi 源自 Bill Joy，grep 源自 Ken Thompson，两者的老祖宗都是 ed，这自然也不是件令人意外的事。

如果使用 grep 命令的话，我们的命令大致如下所示：

:grep '要查找的符号' 文件名列表

当然，grep 支持的复杂参数我们都可以用上。比如，下面的命令可以在所有的子目录里查找用到了 printf 的 .c 和 .h 文件：

:grep -R --include='*.c' --include='*.h' '\<printf\>' .

小提示：在查看搜索结果时，适时使用 zz（或 zt、zb）重定位当前行在屏幕上的位置，可能可以更清晰地查看前后的相关代码。

异步支持

上面这些命令，都有一个缺点：在执行过程中你干不了其他事情。对于执行过程可能较慢的 make，这个问题尤其严重。幸好，在 Vim 8 支持异步任务之后，这个问题也得到了解决。我们利用一个插件，就可以获得类似在一些集成开发环境中的体验，在构建过程中仍然可以继续做其他事情。

我们首先需要安装一个插件 asyncrun.vim。跟前面类似，假设我们使用 minpac 的话，我们需要在 vimrc 中的合适位置加入下面这行：

call minpac#add('skywind3000/asyncrun.vim')

我们还需要一个跟 :make 相似的命令。我使用下面的命令定义（今天我们重点看使用，定义的细节就不讨论了）：

" 和 asyncrun 一起用的异步 make 命令
command! -bang -nargs=* -complete=file Make AsyncRun -program=make @ <args>

这个命令同样会使用 makeprg 选项。不过，还有个问题是默认情况下屏幕上看不到执行过程的信息。我们可以让 asyncrun 在执行命令时立即打开 quickfix 窗口：

" 异步运行命令时打开 quickfix 窗口，高度为 10 行
let g:asyncrun_open = 10

对于 C/C++ 程序员来讲，启动和停止构建应该是一个很频繁的操作吧。所以，我也给它分配了一个快捷键：

" 映射按键来快速启停构建
nnoremap <F5>  :if g:asyncrun_status != 'running'<bar>
                 \if &modifiable<bar>
                   \update<bar>
                 \endif<bar>
                 \exec 'Make'<bar>
               \else<bar>
                 \AsyncStop<bar>
               \endif<CR>

上面的代码通过判断异步任务状态和窗口是否可修改，还会自动执行保存文件和终止构建等操作。建议你自己尝试一下。鉴于我们本讲内容已经很多了，我们暂时就不讲解了。

演示异步启动构建和使用快捷键跳转

查看文档

Vim 里快捷键 K 可以用来查看光标下关键字的相关文档。它的行为是由选项 keywordprg（:help 'keywordprg'）控制的。这个选项的缺省值是 man，表示查看 Unix 的 man 手册，很多文件类型插件会对当前缓冲区设置一个更合适的值，如 Vim 脚本就会直接把行为改成调用 :help 命令。

查看 man 手册的默认行为通常只在终端工作良好，而在图形界面 Vim 里会出现显示问题。我推荐使用 Vim 内置的 man 插件，并把全局的 keywordprg 设成 :Man：

" 启用 man 插件
source $VIMRUNTIME/ftplugin/man.vim

set keywordprg=:Man

这样，我们在使用 K 命令时，将在 Vim 里直接打开 man 手册，效果如下所示：

使用 K 在 Vim 里查看 man 手册

你看，是不是就方便多了？

内容小结

今天我们讨论了 Vim 中对编程的基本支持，包括：

Vim 使用编程规则来判断文件类型，逻辑放在文件 filetype.vim 里。

Vim 里有很多设置文件格式的选项，自动设置一般在 ftplugin 和 indent 目录下；我们可以简单地在 vimrc 配置文件中进行定制。Vim 通过 cindent 和 cinoptions 选项，对类 C 的语言提供了相当细颗粒的缩进风格支持。

Vim 对 tags 文件提供了完整的支持，Ctags 工具可以对超过 40 种主流编程语言生成 tags 文件，供 Vim 和 Tagbar 使用。

Vim 里通过 quickfix 窗口，对构建和搜索提供了内置支持；从 Vim 8 开始，我们可以使用异步支持，在构建时继续进行编辑。

Vim 里通过 K 命令，可以快速地查阅文档；通过 man 插件，我们可以直接在 Vim 里查阅 man 手册。

本讲我们的配置文件更改较多，请仔细检查一下其中内容。对应的标签是 l8-unix 和 l8-windows。

课后练习

今天的内容不难，但较多较杂。请你务必自己试验一下我上面总结的这些功能，来加深对这些功能的理解。要提高编辑的效率，熟悉基本功能一定是必要的。

不管你用什么语言编程，找出 Vim 是如何判断你的源代码文件类型的。

看看你能不能找出 Vim 对你使用的编程语言有没有附加的设置（可选）。

尝试一下 Ctags，并用 <C-]> 来跳转到某个符号的定义。

如果你的语言有构建过程，尝试异步的构建过程。

尝试使用 :grep 命令来在你的源代码中搜索某一符号。

对于 C 系语言，尝试使用 K 命令来查阅文档。

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

09｜七大常用技巧：让编辑效率再上一个台阶

你好，我是吴咏炜。

学到这里，你应该已经初步掌握 Vim 了。我们这一讲来重点看一下 Vim 里的七大常用编辑技巧。有些技巧你直接就可以用，有些则需要安装第三方插件。但无论是哪种情况，它们都可以大大提高你的编辑效率。

从这一讲开始，我们将不再讨论插件的安装过程，而只是给出像 skywind3000/asyncrun.vim 这样的名称。相信你学到现在应该已经不需要我再详细讲述这样的基础知识了。如果你对安装插件还不太熟练的话，请复习第 4 讲。接下来，我们正式开始今天的内容。

自动完成

自动完成是一个编辑器中很主流的功能了。通常，我们希望编辑器能在我们输入一部分内容时就能猜到我们希望输入的是什么，并能够予以提示。自动完成可以节约我们输入的工作量，是一件编辑中非常必要的利器。

Vim 内置有自动完成功能。最基本的自动完成功能有两种：

基于当前文件文本的自动完成

基于文件系统的自动完成

我们先说基于当前文件文本的自动完成。在当前文件里，或当前文件用 #include（C 类语言的情况）包含的文件里包含某个关键字时，你可以输入头若干个字母并按下 <C-P>（表示 previous）或 <C-N>（表示 next）来进行自动完成。这两者的区别是，<C-P> 是从当前位置往前找，而 <C-N> 是从当前位置往后找。当只有一个匹配项时，Vim 直接给出完成结果，再次按下 <C-P> 或 <C-N> 则取消自动完成。当存在多个匹配项时，Vim 会根据搜索顺序给出匹配项列表并使用第一个匹配项；再次按下 <C-P> 或 <C-N> 则可以在列表里进行选择。

Vim 的缺省选项能帮你在 Unix 系统上找到系统的头文件，利用里面出现的关键字来完成。想要在其他语言或平台里找到当前文件“包含”的文件里的关键字，请参考下列选项帮助：

:help include

:help includeexpr

:help isfname

:help path

自动完成的示例

我们再看一下基于文件系统的自动完成。当你在插入模式下输入一个绝对路径或者当前目录下的文件 / 目录名称的一部分时，你可以使用 <C-X><C-F> 来启动文件自动完成。在此之后，操作就和前面一样了，你可以使用 <C-P> 和 <C-N> 在匹配项中跳转和取消。

Vim 里还有其他一些以 <C-X> 开始的自动完成功能。比如，你可以用 <C-X><C-K> 从配置的词典中选择合适的单词，可以用 <C-X><C-O> 进行“代码自动完成”。但这些功能要么不常用，要么在缺省配置下工作得并不好。所以，今天我就暂时不讨论其他自动完成功能了。等到了提高篇和拓展篇，我们再来看英文文本编辑和代码自动完成这两个话题。

最后，要注意任何自动完成功能都可能会重复你的错误。如果你一开始拼错了，后面又拼对了，很可能会发现前面的错误。而一旦使用自动完成，你要是一开始就拼错了，后面可能就会不断重复之前的错误。这当然不是编辑器的错，但作为我曾经见到发生过的问题，我觉得值得提醒你一下。

文本目标跳转

如果光标下面是一个计算机可以找到的文件，你一定希望我们有办法可以一下子打开这个文件吧。这正是我们这一节要讨论的技巧。

当光标下的文件名可以在 path 选项标识的目录下找到时，我们可以很方便地跳转过去。你需要的是正常模式命令 gf 和 <C-W>f。估计你很容易猜到，前者是直接跳转到文件（理解为“goto file”），后者则会打开一个新窗口（window），在新窗口里打开该文件。

文件跳转的演示

如果光标下面是一个链接，或者非文本文件，那我们该怎么办呢？显然，即使 Vim 可以打开这个文件，看到的内容也多半不是你想要的（你想看图片，还是把图片当成文本的乱码？）。这时候，最简单的解决方式是使用 netrw 插件提供的 gx 命令。它的缺省行为是使用操作系统提供的机制来打开光标下的文件或链接。

比较让人伤心的是，最新版本的 netrw 插件在打开链接时的行为不正常。这个问题已经报告有一年了，还没有解决。作为临时方案，我在 Vim 配置的目录放了一个可以工作的老版本，你可以把这个文件复制到你的 Vim 配置目录下的 plugin 子目录下来绕过这个问题。此外，gx 只适合本机，不适合在远程连接上使用。

Vim 寄存器 / 剪贴板

我们已经学到，Vim 的删除和复制命令（如 d 和 y）会把内容存起来，以供粘贴命令（如 p 和 P）使用。我们还没有讨论这种内容存储有什么特别的地方。

首先，估计你已经知道的是，Vim 把要粘贴的内容存在 Vim 内部的“寄存器”（register）里，而非系统的剪贴板。你不一定知道的是，Vim 里的寄存器有好多个。事实上，Vim 有超过 40 个不同的寄存器！我们挨个来看一下：

首先是无名寄存器。当操作没有用 " 加寄存器名称指定寄存器时，我们默认使用无名寄存器。不过，我们仍可以使用 "" 来指定使用无名寄存器，也就是说，""p 和 p 效果相同。

其次是数字寄存器 0 到 9。0 号寄存器中放的永远是最近一次复制（yank）的内容。这和无名寄存器很不一样，它里面放的是最近操作的结果，也包括了 d、x、c 等命令，特别是包括了粘贴命令所替换的内容。1 到 9 号寄存器中放的则是上一次、倒数第二次、直到倒数第九次被删除或修改命令删除的文本。在做少量的用一个名字替换另一个名字、而又懒得使用替换命令时，"0p 是一个接近图形界面里的粘贴命令的常用选择。

然后有小删除寄存器 -。上面我说得不全，删除内容进入 1 到 9 号寄存器的前提条件是被删除的内容至少有一行，或者使用了移动命令 %、(、)、`、/、?、n、N、{ 和 } 进行删除。否则，删除的内容只会进入 - 而不是 1 到 9 号寄存器。

常用的有名寄存器 a 到 z。这些寄存器仅在用户手工指定时才会使用，内容在下一次打开 Vim 时仍然存在。比如，我们可以用 "ayy 代替 yy 把当前行复制到 a 寄存器中，以后就一直可以用 "ap 来进行粘贴了，直到 a 寄存器的内容被替换为止。

不常用的特殊寄存器 . 、:、# 和 %。这些相对来说不那么常用，请自行查看帮助文件 :help ". 等。

黑洞寄存器 _。专门用来删除，目的就是不要影响无名寄存器的内容。

搜索寄存器 /。存放是上一次搜索使用的模式。

表达式寄存器 =。可以把 Vim 表达式估值的结果作为寄存器的内容。这个我们以后讲 Vim 脚本编程的时候再探讨。

最后是图形界面剪贴板寄存器 +、* 和 ~。一般而言，+ 寄存器代表操作系统的剪贴板，和图形界面应用程序交互用这个就好；你用图形界面 Vim 菜单里的拷贝和粘贴访问的也是系统剪贴板。* 和 ~ 在 X11 和 GTK 环境下有一些特殊用途，我们目前就不展开了。想深入钻研的话，可以查看帮助文档 :help "+、:help "* 和 :help "~。

寄存器在正常模式下可以用 d、y、p 等命令来访问，你现在应当已经很清楚了。它们在插入模式和命令行模式下也可以用 C-R 加寄存器名来访问，这经常也会省去你很多打字的麻烦。

这些寄存器当然不是每个都常用。具体你是否会用到它们，取决你的工作方式。下面我说几个我自己编辑时的常用场景。

常用的寄存器使用场景

如果要交换两行内容，可以直接利用删除命令会把删除的内容放到无名寄存器这个特性。我们在第一行上面按下 dd，然后直接按 p 粘贴即可。

如果要交换两处文本内容，可以类似地使用删除和粘贴替换都会把内容放到无名寄存器这个特性。我们选中第一处文本，按下 d 进行删除；然后选中第二处文本，按下 p 进行粘贴；最后回到第一处文本的原来位置，使用 P 把文本粘贴回去即可。

两处文本交换的演示

如果要少量修改某一变量名称（多的话使用 :s 替换命令更合适），可以把光标移到变量名称上，用 * 进行开启自动搜索，然后编辑变量名称到合适；随后复制新的变量名称，反复使用 n 命令搜索，并用 ve"0p 进行替换即可。

当然，反复打 ve"0p 真的会感觉这个命令有点长。鉴于这个组合键使用的频度还挺高，我觉得映射一个更短的按键比较好，我的选择是 \v，同时，我做了点更通用的处理：

" 替换光标下单词的键映射
nnoremap <Leader>v viw"0p
vnoremap <Leader>v    "0p

关于 <Leader> 的含义，可查看帮助文档 :help <Leader>，里面说得很清楚，我就不重复了。如果你忘了 viw 的意义，请复习一下第 3 讲里的文本对象。

使用复制、自动选择、粘贴修改变量名

宏的录制和播放

Vim 里可以用 q 把动作记录到寄存器里，然后使用 @ 来播放这些动作。上面这个变量更名，如果用宏来做也可以：

用 * 开启搜索

键入 qa 开始录制宏到 a 寄存器；当然我们可以使用其他寄存器，只要被录制的命令不会修改这个寄存器即可，所以一般使用 a 到 z 这 26 个有名寄存器

键入 n 进行搜索；先行搜索的目的是，如果搜索不到内容，命令出错，宏的剩余部分就不会被执行

键入 eabar<Esc> 把 foo 修改为 foobar

键入 q 结束宏录制

键入 @a 播放录制的宏

重复上一步直到 Vim 报告找不到 foo 为止

使用宏修改变量名

关于宏的进一步细节可以查看帮助文件（:help q），我就不展开了。

从上一节的 \v 到宏再到 :s 命令，对我们当前的任务而言，自动化程度逐步上升，但交互性逐步下降，“僵硬”性也逐步上升。对于重复遍数较多、信心较高的修改，我们应当偏向使用更自动化的方式，对于重复遍数较少或信心较低的修改，我认为使用不那么自动化的方式更有助于实时检查修改的效果。

今天关于寄存器和复制粘贴我们就讲到这里。我们以后还会有讨论到寄存器的时候。

文本对象增强

Vim 对文本对象的支持我已经在第 3 讲里讨论过了。那些当然是很不错的功能，不过，能不能在那些功能的基础上再进一步，做出更有用的功能呢？对于写了多个 Vim 插件的 Tim Pope 来说，答案是肯定的。

具体来说，如果你安装了他的 tpope/vim-surround 插件，你可以实现下面这些功能：

在一个单词的外面加上引号，如把 word 变成 "word"，可以使用命令 ysiw"

把一个单词的外面的双引号变成单引号（有强迫症的 Python 程序员很可能有这样的需求），如把 "word" 变成 'word'，可以使用命令 cs"'

把外面的引号或括号变成 HTML 标签也没有问题，如把 [my choice] 变成 <em>my choice</em>，可以使用命令 cs[<em>

可视模式也有类似的命令，如可以在选中 my choice 后，输入 S<em> 把文本变成 <em>my choice</em>

当然，你也可以把加上的包围符号移除，命令是 ds 后面跟包围符号，如 ds" 可以移除外围的双引号；要移除 HTML 标签则使用 t 来表达，即使用 dst 来移除文本外面的第一个 HTML 标签

注意 Vim 命令 . 只能用来重复 Vim 的内置命令，而不能用来重复上面这样的用户自定义命令。为了解决这个问题，我也会安装 tpope/vim-repeat 插件，使得重复命令对上面这样的情况依然能够生效。

Vim-surround 和 vim-repeat 的演示

撤销树

Vim 不仅支持多级撤销，而且有撤销树的概念。利用撤销树，你可以转回到编辑中的任何一个历史状态。不过，问题是，Vim 用来管理撤销树的命令不那么直观。在使用撤销树的图形化插件之前，我自己也没有把相关的命令真正用好。

著名的撤销树插件我知道两个，一个是 mbbill/undotree，一个是 sjl/gundo.vim。两者功能相似，界面风格和快捷键有所不同。鉴于 undotree 功能更加丰富，我就以它为例来介绍一下。

从下图中可以看到，undotree 可以展示完整修改历史。你可以用 J 和 K 在历史中跳转，左下角的预览窗口中就会显示修改的内容，右侧文件直接会回到相应的历史状态，并加亮最近的那次修改。一旦用上这个插件，就真的回不到没有这个插件的环境了。

Undotree 插件的效果

另外需要稍加注意的一点是，一旦这个文件在其他编辑器里修改了，Vim 发现内容对不上，就无法保留编辑的历史。有一个绕过方法是，当你需要使用其他编辑器修改前，确保你在 Vim 里打开了该文件并且所有修改已保存；这样，在修改完成之后，只要在 Vim 里用 :e 命令重新载入该文件，Vim 就可以把外部的修改也保存在撤销历史记录里，保留完整的编辑历史。此外要注意的是，最后得在 Vim 里使用 :w 存盘一次，才能把编辑历史真正保存下来——即使你在 Vim 里没有进行任何修改，也需要这样做一下才能保存修改的历史。

对当前缓冲区的更名和移动

你肯定遇见过文件需要更名或者移动吧。这当然很简单，你可以通过图形界面或命令行进行操作。但这样操作之后，有一个问题是 Vim 的撤销历史跟文件就再也对不上了，你也没法再继续撤销更名或移动前的编辑操作了。有一个 Vim 插件，也是 Tim Pope 写的 tpope/vim-eunuch，可以解决这个问题。

事实上，这个插件的功能远不止更名和移动。它实际上是把 Unix 的很多命令行工具搬到了 Vim 里（比较一下 Unix 和 eunuch 的发音你就知道这个插件的名字是什么意思了）。对我来说，最重要的就是它提供的 :Rename 和 :Move 命令，后面跟的参数就是新的名字或路径。这样操作之后，以后再打开这个更名或移动后的文件，仍然能够访问它的一切编辑历史。

模糊文件查找

使用 NERDTree 的话，你可以通过浏览目录来打开文件。这种方式，对于你知道文件在哪个目录下、但不知道文件名的时候特别有用。另外一种可能的情况是，你知道文件名或其中的关键部分，但你不知道或不关心文件在哪里。这种情况下，Fzf 的模糊匹配就非常有用了。我们先来看一下动画演示，有一个初步的印象：

Fzf 的动态文件筛选演示

从动画可以看到，插件使用的是模糊匹配的方式，可以动态展示搜索的结果，并能直接预览当前选中的文件内容（在窗口足够宽的情况下）。因而这种方式不仅快，而且非常直观。

跟其他插件不同的是，fzf.vim 插件依赖于 fzf 命令行工具。在 fzf 的页面上列出了具体安装方式，支持各个平台。但由于这个软件比较新，老一点的 Linux 发布版（如 Ubuntu 18.04 和 CentOS 7）在包管理器里还没有 fzf。所以我的安装建议是：

对于 macOS，使用 Homebrew 命令 brew install fzf 安装。

对于 Ubuntu 19.10 或更新版本，使用 sudo apt-get install fzf 安装。

对于较老的 Ubuntu、CentOS 7 和其他页面中没有列出的 Linux 发布版，直接使用二进制发布版本，一般使用后缀为“linux_amd64.tgz”的文件。

对于 Windows，也使用二进制发布版本，使用后缀为“windows386.zip”（32 位）或“windows_amd64.zip”（64 位）的文件。

在安装了 fzf 后（可以执行 fzf 来验证一下，它会枚举当前目录下的所有文件，并在你输入字符时缩小匹配；按 <CR> 选择文件，按 <Esc> 取消选择），就可以安装插件了。使用 minpac 的话，我们需要在 vimrc 中加入下面两行：

  call minpac#add('junegunn/fzf', {'do': {-> fzf#install()}})
  call minpac#add('junegunn/fzf.vim')

在安装完成之后，你就可以像我前面展示的那样使用 :Files 命令了。更多高级用法可以查看 fzf.vim 的页面。

顺便说一句，如果你对安装一个可执行文件有点发怵的话，插件也可以自动帮你下载 fzf 命令。但这样做的缺点是，fzf 就只能在 Vim 里面使用了。如果你使用包管理器安装或手工安装，fzf 可以在 Vim 里使用，也可以在 Bash 等其他地方使用——fzf 的 Bash 集成是可以大大提升 shell 的使用体验的，不过这不属于我们 Vim 课程要讨论的话题，就请你自行参阅文档了。

这个插件可以跟其他工具进一步配合。如果你安装了 ripgrep 和 bat 的话，可以获得更好的效果。动图中右下角文件预览的语法加亮的效果就依赖于系统里有 bat。如果你装了 ripgrep 的话，可以考虑设置下面的环境变量：

export FZF_DEFAULT_COMMAND='rg --files --sortr modified'

这样的话，fzf 可以利用 ripgrep 来自动过滤掉被 Git 忽略的文件、隐藏文件、二进制文件等程序员通常不关心的内容，并将结果以修改时间倒排，确保最新修改的文件在最下面，大大提高了迅速找到你需要的文件的概率。

内容小结

今天我们讲述了不少编辑中的技巧。鉴于这些内容比较散、单项内容又比较小，我在这儿只对它们适用的场合作一下快速总结：

Vim 里有自动完成功能，可以让你只输入文本或文件名 / 路径的一部分，让 Vim 来帮你完成剩余部分。

反过来，对于文件中出现的文件名和超链接，Vim 也支持打开它们。

Vim 里的寄存器相当于几十个不同用途的自动剪贴板，用好它们，能更加高效地完成常见的编辑动作。

Vim 里的文本对象是个特色功能，vim-surround 和 vim-repeat 插件又对其进行了进一步增强。我觉得这对前端程序员和 Python 程序员会特别有用。

跨会话撤销已经很强大了，而撤销树则让你能够充分发挥这个强大功能的潜力。

Vim-eunuch 插件可以让你在对文件进行更名和移动时仍然保留其编辑 / 撤销历史。

Fzf.vim 插件提供若干快速查找文件的工具，它的最基本命令 :Files 可以让你使用部分匹配的文件名快速地在当前目录或指定的目录下面的任一目录里找到你需要的文件。

本讲我们的配置文件修改也不少，包含了我们今天讲到的这些插件。对应的标签是 l9-unix 和 l9-windows。

课后练习

今天讲的主要是技巧，而非理论知识，所以最主要的课后练习，就是需要自己实践一下，把需要安装的插件也全部都装起来。如果有任何问题，可以留言和我讨论。

希望你根据你的实际使用场景，可以举一反三、融会贯通。比如，我讲到了交换两行，你能不能也能做到交换两列的内容呢？学以致用是掌握 Vim 这样的工具的唯一方法。

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

10｜代码重构实验：在实战中提高编辑熟练度

你好，我是吴咏炜。

在前几讲中，我们已经学了很多关于 Vim 的知识，现在需要好好消化一下。今天是基础篇的最后一讲，我们就基本上不学新的内容了，而是通过一个假想的代码重构实验，来复习、巩固已经学到的编辑技能。

开始前的准备工作

这是一堂实验课，你需要跟着我一步步地操作。跟只学习文字内容相比，实践操作能让你收获更多。所以，就请你现在把电脑准备好，跟我来吧。

今天我们将要做的是，签出我为极客时间写的 C++ 示例程序，并对其中的代码进行重构。别紧张，你不需要精通 C++，因为我会在必要的时候对代码进行解释。你学习的重点在于，我是如何进行编辑的，而不是我写的代码是什么意思。

首先，你需要先为工作代码找一个合适的父目录，然后用下面的命令签出代码（Windows 下面去掉“\”全部写一行，或者把“\”换成“^”））：

git clone --recurse-submodules \
          --shallow-submodules \
    https://github.com/adah1972/geek_time_cpp.git

万一我以后更改代码的话，就有可能造成内容或路径发生变化。所以，请把我们今天编辑的 commit id 记下来：632b067。如果你用 git log 看到 HEAD 的 commit id 不是它，可使用 git checkout 632b067 这个命令来签出跟今天完全相同的版本。

下面，我们就开始了！

类模板 smart_ptr 更名

我们第一步要做的，是把示例的 smart_ptr 类模板更名为 shared_ptr。同时，为了避免跟标准的 shared_ptr 发生冲突，我们要把它放到名空间 gt里面去（当然，你可以用其他名字；这只是我们的示例）。

大体思路是，先需要找到 shared_ptr 定义所在的文件，对其进行修改；然后找到使用该文件的地方，也进行相应的修改。下面我们就来做一下。

修改类定义

首先，我们需要进入 geek_time_cpp 所在的目录。如果你前面的命令就是 git clone 的话，那现在使用 cd geek_time_cpp 就可以了。

然后，我们当然是启动 Vim 了。假设我们知道 smart_ptr 被定义在 smart_ptr.h 头文件里，那我们最快的打开方式就是使用 :Files 命令，然后输入“sm”，即可看到“common/smart_ptr.h”成了第一选择。我们此时按下回车键即可打开文件。

使用 fzf.vim 插件打开文件的界面

进入文件后，我们先来看一下文件的结构。根据目前的 Vim 配置，我们可以使用 <F9> 打开 tagbar 插件。注意，这个文件使用了 C++11，Exuberant Ctags 会有错误的识别。下面的截图是安装了 Universal Ctags 之后的结果：

使用 tagbar 查看文件结构

我们可以看到这个文件比较简单，里面主要就是两个类的定义和一些全局函数。不过，我们还是要确认一下，文件中没有任何会被错误匹配替换的内容。我们可以在右侧窗口里双击“smart_ptr”，这样左侧窗口就会跳转到 smart_ptr 的定义上，并且光标停留在类名上面。这样，我们只需使用 * 启动搜索和加亮即可。使用 n 继续搜索，我们很快就能确认文件中确实没有冲突的内容。

下面，我们进行替换操作，需要键入的是 :%s/<C-R>//shared_ptr/g<CR>（<C-R> 和 <CR> 都是按键，而非小于符号后面跟其他字符）。我们不需要手工输入 \<smart_ptr\>，因为搜索寄存器 / 中已经有我们要的内容了。

键入替换命令

最后，我们在第一个类定义的前面加上 namespace gt {、在最后的 #endif 前面加上 } /* namespace gt *／，就完成了定义的修改。

不过，现在文件名还没有更改，文件里的包含保护（即宏 SMART_PTR_H）也没有更改。包含保护需要简单的重命名，就请你用我们目前介绍的任一方法自己完成了。随后，我们用命令 :Rename shared_ptr.h 即可完成更名和存盘操作。

修改使用 smart_ptr 的地方

我们先试着用下面的命令搜索一下：

:grep -R --include="*.cpp" --include="*.h" "\<smart_ptr\>" .

（小提示：在查看搜索结果的时候，适时使用 zz、zt 和 zb 命令，可以把周边的代码看得更清楚。）

使用 :cn （或我们定义的快捷键）仔细检查搜索出来的结果，我们会发现有一些误匹配：有 smart_ptr 是 unique_ptr 的情况，也有 smart_ptr 是策略类的情况。

我们稍微改换一下方法，搜索对 smart_ptr.h 的使用：

:grep -R --include="*.cpp" --include="*.h" "\<smart_ptr\.h\>" .

这样的话，我们会发现结果只有一个匹配，那就简单了。

在上一讲里，我们已经讨论了在这种情况下进行修改的三种不同方法（忘了？请回过去复习一下）。今天，我们用第四种方法。这种方法的每一步我们实际上都讲过，但串起来用，你可能就没有试过了。我们使用的基本命令是 cw、n 和 .。

由于之前搜索过 smart_ptr，我们现在仍然可以继续使用 n 找到需要修改的地方。我们随即需要键入的，是 cwgt::shared_ptr<Esc>。这样输入虽然有点长、有点啰嗦，但它的好处是整个修改会被 Vim 看作是一步，因而可以用 . 命令来重复。这样，下面我们只需要反复利用 n 和 . 命令，把除了 #include 那行之外的所有 smart_ptr 都改成 gt::shared_ptr 即可。

使用 cw、n 和 . 来进行替换

很显然，这并不是唯一的方法，也不一定是最好的方法。所以，我建议你在这里暂停一下，用 :e! 重新载入这个文件，试试使用上一讲提到的其他方法。我这里就仅仅再给你展示一下如何使用替换命令，同时又不会误匹配文件名：

:%s/\<smart_ptr\>\ze\%([^.]\|$\)/gt::shared_ptr/g

这个匹配模式说的是，我要查找完整的单词“smart_ptr”（这就是要替换的内容了），但是，在匹配结束（\ze）后，我还有两个额外的匹配要求（用 \%( 和 \) 括起来），要么不是句点（[^.]），要么（\|）是行尾（$）。

我们最后把唯一残留的 smart_ptr.h 修改成 shared_ptr.h，就完成了 smart_ptr 的更名任务。

编译执行（可选）

如果你懂 C++，并且有 geek_time_cpp 的 README 文件里要求的执行环境的话，可以选择体验一下编译执行。

我们需要先在 02 目录下创建并进入 build 子目录，然后运行 cmake ..。随后，在 Unix 环境下，一般可立即使用快捷键 <F5> 进行编译；想要在 Windows 下也能正常进行编译，我们则应当设置 set makeprg=cmake\ --build\ .\ -j（老版本的 cmake 可能不支持 -j 命令行参数的话，这样的话，我们会没法用 cmake 进行并发编译；不过对于我们的小例子没啥关系）。

另外一个要注意的地方是，Vim 在缺省配置下不能识别 Visual C++ 的错误输出格式 。为了能进行识别，并在发生错误时跳转到文件的指定位置，我们需要设置下面的选项：

set errorformat=\ %#%f(%l\\\,%c):\ %m

目前来讲，环境没问题的话，我们就会……遇到编译错误。

Windows 下遇到编译错误的界面

原因是 dynamic_pointer_cast 前面也需要加上 gt::。做了这个修改之后，我们就应该可以顺利编译出可执行文件了。在 Windows 下使用命令 :!.\Debug\sp_test02_shared_ptr，或在 Unix 平台下使用命令 :!./sp_test02_shared_ptr，我们即可在终端看到下面的输出：

circle()

use count of ptr1 is 1

use count of ptr2 was 0

use count of ptr2 is now 2

ptr1 is not empty

use count of ptr3 is 3

~circle()

同时，如果愿意的话，我们也可以使用 AsyncRun 提供的机制，在 Windows 下使用命令 :AsyncRun .\Debug\sp_test02_shared_ptr，或在 Unix 平台下使用命令 :AsyncRun ./sp_test02_shared_ptr，异步运行程序并把输出重定向到 quickfix 窗口里。

添加跟踪语句

假设我们对这个代码执行过程有些疑问，想添加些跟踪语句，该怎么做呢？

我们首先需要在一个新窗口中打开 common/smart_ptr.h。由于我们第一个打开的文件就是它，所以它的缓冲区编号为 1，我们可在用 <C-W>n 打开一个新窗口后，使用 1<C-^> 飞速地重新打开文件。

我们希望对引用计数的增、减、删除等操作进行跟踪。最简单的方式，当然就是执行对应操作的时候，把执行的语句也输出一下。像这样简单的机械化操作，显然就是宏的天下了。我们来试一下。

我们先来改造一下 smart_ptr 析构函数里面的第一个 delete ptr_。一个可能的操作步骤是：

复制当前行

粘贴当前行

选中行首缩进后、结尾分号前的内容，套上双引号

在这个新对象前后插入输出所必须的命令

我们需要录制的宏的内容是 yyPv$hS"gvS)iputs<Esc>l%a;<Esc>，而你把这一串东西用 nmap 命令映射给某个按键上也完全可行（注意，此处不能用 nnoremap，因为我们需要使用 vim-surround 插件带来的新的 S 按键的定义）。当然，在交互的环境中，录制按键会比眼睛看这个字符串容易理解多了。Vim 的宏，就其本质而言，可算是一种只写不读的简单过程式语言。

录制把一行语句变成调试输出的宏

我们用到的命令里，只有 gv 是之前没有学过的。我们当然也有其他方法来选中行中的内容，但 gv 的作用是重新选中刚才选中的内容，最快，也最方便。

利用这个宏，我们可以把添加调试语句变成按两个键。哦，对了，宏一旦执行过后，第二次执行同一个宏只需要键入 @@ 即可，这样还能更快些。

在我们把所有的 delete 语句和 add_count 函数调用行上执行了这个宏之后，我们运行程序可以得到下面的结果：

circle()

use count of ptr1 is 1

use count of ptr2 was 0

other.shared_count_->add_count();

use count of ptr2 is now 2

ptr1 is not empty

other.shared_count_->add_count();

use count of ptr3 is 3

delete ptr_;

~circle()

delete shared_count_;

如果想对这个代码作进一步调整，类似操作即可，相当容易吧？

调整测试用例

我们现在使用鼠标点击或者 <C-W>j 等命令跳转到测试代码 test02_shared_ptr.cpp 中。我们随即使用 <C-W>_ 命令来最大化窗口，因为似乎暂时用不着编辑 smart_ptr.h（但还不那么确定，否则就可以直接关闭那个窗口了）。

我们打算在 ptr1 不为空的那个条件判断下面再加点内容。那行输出看着也挺无聊的，我们就直接把它干掉了。我们可以在那组大括号内的任意地方点击后，使用 ci{ 开始编辑，然后输入以下内容：

printf("ptr1 %s ptr2\n",
       ptr1 == ptr2 ? '==' : '!=');

代码编译居然有奇怪的告警出现……我是 Python 写多了，脑子没转回来吗？没关系，在第一处单引号内部键入 cs'"，然后在第二处单引号内部键入 . 重复一下就好，现在代码应该是正确的了：

printf("ptr1 %s ptr2\n",
       ptr1 == ptr2 ? "==" : "!=");

再次编译，完美，没有问题了！运行程序，我们得到：

circle()

use count of ptr1 is 1

use count of ptr2 was 0

other.shared_count_->add_count();

use count of ptr2 is now 2

ptr1 == ptr2

other.shared_count_->add_count();

use count of ptr3 is 3

delete ptr_;

~circle()

delete shared_count_;

内容小结

今天我们尝试对一小段 C++ 代码进行了简单的重构。在这个过程中，我们使用和复习了下面这些编辑技巧：

使用 fzf.vim 来根据部分文件名迅速打开文件

使用 tagbar 来浏览文件的结构

使用 vim-eunuch 来进行文件更名

使用替换命令来进行批量代码更名

使用 . 命令技巧来进行批量代码更名

使用 <C-R> 在插入模式和命令行模式中使用寄存器的内容

使用 :grep 命令在文件中进行文本搜索

使用异步的构建命令，并设置选项使得错误信息解析在 Visual Studio 工具里也能工作

使用文本对象命令对用括号、引号等符号包起来的文本进行统一的修改

使用宏，在一次操作之后，在遇到类似场景时可以快速修改

虽然今天的代码是 C++ 的，但这些编辑方式适用于任何语言。请你一定要牢牢掌握。我们也应该慢慢看到了，编辑的一个要点，在于把需要重复的工作自动化和简单化。Vim 作为一个程序员的编辑器，提供了灵活而强大的编辑机制——最终用户，或扩展包的开发者，都可以利用这些底层机制，使编辑变得更加高效。

本讲我们对 Windows 下的 vimrc 配置文件有一处小修改，对应的标签是“l10-windows”。

课后练习

实验课中的内容你已经一一尝试了吧？请你再向前一步，想一想我们的每次编辑是否可以有不同的执行方式，及哪种方式对你最顺手。Vim 的命令一定是在使用中才能熟练应用的。你不一定要记住所有可能的编辑方式，但每一种最好都至少尝试一次，然后找出最适合自己的、最能牢牢掌握的编辑方式。

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

11｜文本的细节：关于字符、编码、行你所需要知道的一切

你好，我是吴咏炜。

从今天开始，我们进入提高篇和拓展篇的学习。

在提高篇，我会带你对 Vim 的高级用法和技巧进行专项突破，让你可以对 Vim 做深度定制，应对复杂的工作也不在话下。

在拓展篇，我会介绍一些针对性较强的内容，适合特定场景下的 Vim 使用。为了平衡一般性和特殊性，让你拥有更舒适的学习体验，拓展篇将会与提高篇交叉发布。如果一时用不到相关的知识，拓展的内容可以暂时延后学习。但一旦你需要这些知识时，你会发现，哦，原来如此，Vim 是可以这么使用的！

Vim 是一个文本编辑器，很多人甚至把它称为“编辑器之神”。在基础篇中，你已经了解了很多用 Vim 编辑文本的常用技巧。可是你有没有想过，到底什么才算是文本？在提高篇的第一讲，我们就先来细细分析一下，关于文本你需要知道的一切知识。这会让你更好地理解编辑时出现的一些奇怪问题（如“乱码”），并予以恰当解决。

什么是文本

从二元论的角度看，计算机文件可以分为文本文件（text file）和二进制文件（binary file），但这个分法并没有对文本做出清晰的界定。从实用的角度，我们大致可以这么区分：

文本文件里存放的是用行结束符（EOL，即 End of Line）隔开的文本行，二进制文件里则没有这样的明确分隔符

文本文件可以通过简单、直接的算法转换为人眼能够识别的文字，而二进制文件里含有不能简单转化为文字的信息

我这个描述当然还是有点含糊。事实上，计算机判定一个文件是不是文本文件，并不是件容易的事情，特别是在这个文件含有非 ASCII 字符的时候。曾有一些操作系统（如古老的 Apple DOS），会明确区分文件的类型，但现代的操作系统基本上在文件系统层面完全不关心文件的类型和里面的内容了。因为操作系统不对文件类型进行限定，会更加灵活。

但我们还是需要关心的，因为 Vim 最适合编辑的，就是文本文件了。从实用的角度，我对文本文件的判定通常是：

一个文本文件可以直接输出到终端上，或在简单的编码转换后输出到终端上，显示为一行或多行可识别的字符，并且不包含乱码。

想要理解这句话，你得先知道什么是字符？什么是编码？什么是行和行结束符？下面我就来为你一一解说。

字符和编码

从文件系统的角度看，文件的内容就是一堆比特（bit）而已。把比特对应到字符的方法，就是编码（encoding）。在目前的主流操作系统里，通常八比特是一个基本单位，也就是字节（byte）。最基本的编码方式，就是把一个字节对应到一个字符。

目前的大部分编码方式，在 0－127 的范围里，字节值和字符的对应关系是基本相同的。除了个别字符外，编码的基本方式都和 ASCII（American Standard Code for Information Interchange，美国信息交换标准代码）兼容，如下图所示：

ASCII 字符映射表

注意，头 32 个字符和最后一个字符是控制字符，其中大部分现在已经很少有人使用了，但还有一些我们今天仍然会在不同的场合遇到，如马上就会讨论的 LF 和 CR。

ASCII 是美国标准，里面只有基本的拉丁字母，对其他国家来讲可能就不合适。比如对欧洲国家来说，ASCII 既没有带变音符的拉丁字母（如 é 和 ä ），也不支持像希腊字母（如 α、β、γ）、西里尔字母（如 Пушкин）这样的其他欧洲文字，使用起来很不方便。很多其他编码方式使用了 128－255 的字节值范围作为扩展，总共最多是 256 个字符，一次允许一套方式生效，称之为一个代码页（code page）。这种做法，只能适用于文字相近、且字符数不多的国家。比如，下图表示的 ISO-8859-1（也称作 Latin-1）和后面的 Windows 扩展代码页 1252（下图中绿框部分为 Windows 的扩展），就只能适用于西欧国家。

ISO-8859-1 和 Windows-1252 里的字符

最早的中文字符集标准是 1980 年的国标 GB2312，其中收录了 6763 个常用汉字和 682 个其他符号。至于我们平时用到的编码 GB2312，它更准确的名字其实是 EUC-CN，是一种与 ASCII 兼容的编码方式。它用单字节表示 ASCII 字符而用双字节表示 GB2312 中的字符；由于 GB2312 中本身也含有 ASCII 中包含的字符，在使用中逐渐就形成了“半角”和“全角”的区别。

国标字符集后面又有扩展，这个扩展后的字符集就是 GBK，是中文版 Windows 使用的标准编码方式。GB2312 和 GBK 所占用的编码位置可以参看下面的图（由 John M. Długosz 为 Wikipedia 绘制）：

GBK 编码的第一字节和第二字节分布

图中 GBK/1 和 GBK/2 为 GB2312 中已经定义的区域，其他的则是 GBK 后面添加的字符，总共定义了两万多个编码点，支持了绝大部分现代汉语中还在使用的字。

显然，多个不同的编码方式是不利于信息交换的。我们在打开文本文件时看到的“乱码”，最常见的情况就是文件的编码和打开文件的工具以为的编码不同。毕竟，只要出现了非 ASCII 字符，解释方式就多了。对我们来说，常见的情况是 Latin-1/Windows-1252（西欧文字）、GBK（简体中文）、Big5（繁体中文），今天还增加了 UTF-8。

我们终于说到了 UTF-8，它的全称是 8-bit Unicode Transformation Format，8 比特的 Unicode 转换格式。Unicode 自发明伊始，就是为了统一编码问题，但它的最早编码方式，UCS-2，存在两个重大问题：

和 ASCII 不兼容，不能在现有软件和文件系统中直接使用

在储存 ASCII 为主的字符时，存在一字节变两字节的空间浪费

Ken Thompson 在 1992 年和 Rob Pike（罗勃 · 派克）一起发明了 UTF-8，解决了这两个问题（牛人就是牛人啊）。到了今天，UTF-8 已经成了互联网和 Unix 世界里文本文件（含 HTML 和 XHTML）的主流编码方式。但是，Windows 下的文本文件，由于历史原因，可能还大量使用着传统的编码方式（很错误地被叫做 ANSI）；对于中文 Windows，这个传统编码就是 GBK 了。

抛开编码方式的细节（从网上你可以找到足够多的关于 Unicode 和 UTF-8 的资料），我们需要牢牢记住的是，UTF-8 是 Unicode 里最重要的编码方式，可以把一到四字节长度的字节序列映射成为一个 Unicode 字符。目前我们使用的任何字符都可以用 UTF-8 表示，因而 UTF-8 是我们在 Vim 中使用的内部编码（选项 encoding）。我们在第 2 讲中给出 fileencodings 选项设置，就是为了在读写文件时把文件内容进行适当的转换。这个选项表示的是自动检测使用的编码；而在文件被 Vim 载入后，文件的编码会出现在选项 fileencoding 里。如果 fileencoding 选项为空，则表示文件保存时不做任何转换。

关于编码，我们暂时讨论到这里。下面我们讨论一下字符（character）和字形（glyph）。

字符和字形

Unicode 设计时的一个决定，目前看起来有点短视，那就是对中日韩文字中使用到的汉字进行了“统一”。如果字源相同，它们在 Unicode 中就只占据一个编码点。于是，一个字符可能就有多个字形。这个问题，我在第 2 讲中已经展示过了，它也是我们可能需要在图形界面 Vim 中单独设置宽字符字体（guifontwide）的原因。

部分汉字的中文和日文字形对比

跟中文字符集中“半角”和“全角”的概念有点像，Unicode 中也有字宽的概念。和简单的半角与全角的区别不同，Unicode 里除了窄字符和宽字符，还有模糊宽度（ambiguous width）字符。这些字符的宽度根据上下文而定：在东亚文字里一般是宽字符，而在西方文字里一般是窄字符。最常用的模糊宽度字符有（“U+”后面跟十六进制数值是用来表示 Unicode 字符所占编码点数值的通常方法）：

U+00B0：「°」

U+00B7：「·」

U+00D7：「×」

U+00F7：「÷」

U+2014：「—」

U+2018：「‘」

U+2019：「’」

U+201C：「“」

U+201D：「”」

U+2103：「℃」

对于某一特定字体，它们的宽度当然就是确定的；尤其使用变宽字体（大部分英文字体，不同字符宽度不同）时，如在极客时间的正文里，这个模糊宽度没有什么意义。对于使用等宽字体（程序员一般使用的字体，Vim 只能用等宽字体）的文本编辑器，到底是把这些字符显示成跟 ASCII 字符一样的“单”宽度，还是显示成跟汉字一样的“双”宽度，就是一个需要考虑的问题了。

稍微展开一点点，这个模糊宽度，在我们日常生活中还是造成了一点麻烦的。非常常见的一个排版错误，就是由于使用的软件（在中文 Windows 下的）的字体选择规则，西文中的 「’」误用了中文字体展示，导致这个符号展示出来的字间距过宽。一个相反的麻烦，是中文中写「·」希望两侧留空很足，但在另外一些环境下，永远优先选择西文的字体（如大部分的手机操作系统），导致需要手工两侧加空格才能有比较理想的排版效果……

扯远了，这些毕竟不是 Vim 的问题。Vim 里的解决方式是提供选项 ambiwidth，可以设为 single（默认值）或 double，表示 Vim 到底把这些字符的宽度当成是占一个字符还是两个字符，你想怎么样都可以。对于终端 Vim，由于 Vim 不能决定显示的字体，这个选项只能决定光标在这些字符上应当移动的列数，用户必须自己保证在终端里的设定和 Vim 的设定是一致的；否则，可能导致眼睛看到的编辑位置和实际编辑位置不一致。虽然 macOS 的终端应用、Linux 的 GNOME Terminal 和 Windows 下的 PuTTY 都提供了如何处理模糊宽度字体的设定（关键字是“模糊”或“ambiguous”），但鉴于这些软件的字体选择策略，选择“宽”容易导致显示问题，所以我的建议是保留缺省的“窄”设定。

对于图形界面的 Vim，ambiwidth 选项同时也决定了显示这些模糊宽度字符是使用 guifont 选项还是 guifontwide 的设定。在这种情况下，把 ambiwidth 设成 double 才比较有意义：

设置了 guifontwide 之后，两种 ambiwidth 设置的对比

修改 ambiwidth 主要影响的是一行的长度，而 Vim 具有根据行长来进行断行的功能。下面，我们先来看一下什么是行。

行

从 Vim 和 Unix 的角度看，一个文本文件由多行构成，每一行都以一个行结束符（EOL）结束。根据传统习惯，这个 EOL 在存盘时使用的字符是 LF，编码值是 10（U+000A）。

这只是 Unix 格式。常用的还有 DOS 格式（也包括了 Windows），以及老的 Mac 格式。

在 DOS 格式里，行尾就不只使用 LF 这一个字符了，在 LF 前面会多一个 CR，编码值为 13（U+000D）。这个用法的来源是以前的打字机，CR 表示机架归位（carriage return），LF 表示换行（line feed）。在使用 CR LF 作为行结束符的系统里，CR 只负责光标回到第一列，而 LF 负责光标向下一行。

老的 Mac 则使用单个 CR 字符作为行结束符，但苹果从 Mac OS X（2001 年）开始就使用了 Unix 风格的行结束符。所以，目前我们遇到的文本文件，应当都使用 LF 或 CR LF 作为行结束符了。这也是 Vim 的 fileformats 选项的意义：它的默认值通常是 unix,dos（Unix 环境下）或 dos,unix（Windows 环境下），即会自动检测 Unix 和 DOS 行尾；如果检测不到，则以第一个风格设置作为默认值。

fileencodings 有一个对应的文件相关的 fileencoding 选项，跟它一样，fileformats 也对应有一个文件相关的 fileformat 选项，表示当前文件的行尾风格。需要注意的是，如果一个文件里既有 LF 行尾、又有 CR LF 行尾的话，Vim 会把文件当成 Unix 格式，于是文件里会出现最后一个字符显示成“^M”（通常为蓝色，表示是控制字符，跟正常文本不同）的情况。如果你想保留这种行尾，那不需要做任何事情。但绝大多数情况下，你会希望把行尾统一成 Unix 风格或 DOS 风格。此时，你可以使用下面两种方法之一：

使用 :e ++ff=dos 命令强制以 DOS 行尾加载文件；此时文件的行尾格式是 dos。

使用 :%s/\r$// 命令删除行尾多余的 CR 字符；此时文件的行尾格式保持 unix 不变。

此外，再说明一下，Unix/Vim 的传统是任何一行都以行结束符终结，包括最后一行。使用 Vim 编辑的文本文件，最后一个字符通常是 LF（除非使用 Mac 行尾风格，则结尾是 CR）。Windows 上大部分文本编辑器则允许最后一行不以行结束符结束；这样的文件在 Vim 打开时，Vim 默认会给出一个“[noeol]”的提示。在存盘时，Vim 则会自动在最后添加一个行结束符。

除了 Vim，很多 Unix 工具都会有类似的要求。比如，用于文件比对的命令行工具 diff，它在文件比对时如果输出下面的信息，就是表示文件之一没有用行尾结束符来结束：

\ No newline at end of file

断行

中文文本文件的行文习惯，通常是在一段之中不空行，一段结束了再换行。文本编辑器需要做的，是在行长超过屏幕宽度时自动折行。Vim 虽然也能在这种情况下自动折行，但 Vim 的更惯常用法是欧洲字母文字和源代码的做法，行长有一定的限制（根据惯例，常用值是 72、80、120），到了指定的行长则应当进行断行，用一个空行来明确表示分段。这也是 Markdown 格式里的标准做法：单个换行符仅相当于空格而已。（这个额外插入的空格就是中文一段之中不换行的原因。）

Vim 有一个文本宽度的选项 textwidth，表示插入文字时的最大行宽度。这个选项的全局默认值为 0，表示不进行限制，但 Vim 脚本可能会设置它，你也可以自己在 vimrc 等地方对其进行设置。我自己的设置是文件相关的，如：

au FileType changelog  setlocal textwidth=76

这个设置，加上对行进行格式化的命令 gq，可以让你方便地对（英文）文本进行整理。gq 命令跟 c、d 等命令一样，可以先在可视模式下选定文本，也可以在命令之后跟动作键。对于源代码，它的妙处在于它知道什么是注释，什么是列表：

设置行宽为 64、使用数字列表，然后格式化注释

如果对这些功能有兴趣的话，请查看相关的帮助：:help gq 和 :help fo-table。我这儿特别要指出的是：

要能够在无空格的中文之中断行，我们需要有 :set formatoptions+=m

选项 ambiwidth 会影响行宽的判断，如左右弯引号的宽度算 1 还算 2

在 Vim 8.2.0901 之前，Vim 断行时不考虑中文标点符号的规则；要使用 gq 对中文文本断行，最好升级到这个版本或更高版本

编辑二进制文件

到这里，你已经知道什么是文本和关于文本的基本知识了。Vim 当然是一个文本编辑器，但在某些情况下，它也是可以用来编辑二进制文件的。有几个工具在你必须用 Vim 编辑二进制文件时会有帮助。

首先，Vim 有个 binary 选项和一个 -b 命令行参数。当你通过 -b 命令行参数，或 :e ++binary … 命令来打开文件时，binary 选项会自动被设置（用户不应该手动设置该选项）。这个选项保证了，Vim 在读取和存储文件时，不会做会影响文件内容的转换和修改。

不过，即使有这个选项，二进制文件打开后仍然是一堆乱码，这当然是正常的。你除了可以在里面搜索文本之外，还可以利用 Vim 的 Tools（工具）菜单下的“Convert to HEX”（转换成十六进制）和“Convert Back”（转换回）两项，来对二进制文件进行编辑。下面的两张图显示了打开二进制文件后的样子和使用了“Convert to HEX”后的样子：

用二进制模式打开一个 PNG 文件

转换成 HEX 格式后的结果

不管你是要检查文件中的具体字节内容，还是要修改某个字节，HEX 格式都更方便一些。当然，如果你要把修改写回硬盘的话，一定要先使用“Tools > Convert Back”。

如果你有专门的二进制编辑工具的话，Vim 的这个功能可能不那么有用。如果正好你没有安装其他的二进制文件编辑工具，那这个功能还是可以救救急的。

要是你使用的不是图形界面，菜单里的这两个命令可以用 :%!xxd 和 :%!xxd -r 来手工替代。

内容小结

这一讲我们讨论了什么是文本，包括：

文本是用行结尾符隔开的、使用某种特定编码的字符序列

UTF-8 是目前最主流的编码方式，但我们仍然可以对个别文件使用不同的编码

字符和字形在 Unicode 下并不是简单的一一对应，我们需要语言相关信息才能保证正确的显示

目前主流的行尾格式是 Unix 和 DOS，Vim 都支持，并且可以自动判断

Vim 支持设置特定的行宽，然后据此来进行断行，且对代码注释、数字列表等文本形式有特殊支持

虽然 Vim 是文本编辑器，但在需要的时候，我们也可以使用 Vim 来查看和编辑二进制文件

本讲我们对 vimrc 配置文件有一处小修改，对应的标签是“l11-unix”和“l11-windows”。

课后练习

文中提到的内容，你都应该手工尝试一下。除此之外，如果你平时接触到 GBK/GB18030 和 UTF-8 之外的其他文本编码的话，你可以考虑查看一下插件 FencView（注意，在 Windows 下你现在也不需要下载 iconv.dll 了：Vim 8 的 Windows 安装包中现在已经包含了 libiconv-2.dll）。

如果你使用的是 Linux 或 macOS 的话，可以键入 iconv -l 来看一下 libiconv 支持的编码方式：Vim 内部就是使用 libiconv 来实现编码转换的。

过去我们会有一些文件用后缀表示文件编码，比如 .gb 是 gbk 编码，.big5 是 big5 编码，.nfo 是 cp437 编码，你能想出如何正确载入这些文件的方法吗？

今天就到这里了。你的回答或问题，都可以留言告诉我。我们下一讲再见！

12｜语法加亮和配色方案：颜即正义

你好，我是吴咏炜。

语法加亮这个功能，我们都非常熟悉。和 vi 刚出现的时代不同，它现在已经成为编程的基本功能了。在我们使用的各种代码编辑器中，都有语法加亮的功能。我们甚至可以拿一句俗语反过来说：没见过猪跑，还能没吃过猪肉么？

但是，你有没有想过，语法加亮到底是怎么实现的呢？今天，我们就不仅要尝尝不同“风味”的猪肉，还要进一步看看猪到底是怎么跑的——这样，我们才能选择，然后调整出，最符合自己口味的大菜。

语法加亮

在第 8 讲里，我们已经提到，Vim 的语法加亮依靠的是在 syntax 目录下的运行支持文件。今天，我就通过例子给你解说一下，Vim 里如何实现语法加亮，然后语法加亮又如何映射到屏幕上的颜色和字体。

我们先来看一个比较简单的例子，xxd。

xxd 这个名字看起来，是不是有点陌生又有点熟悉？其实，我们在第 11 讲还刚讲过 xxd：它是一个把二进制文件转换成地址加十六进制数值再加可读 ASCII 文本的工具，它的输出格式在 Vim 里也被称作 xxd。不过，在用菜单项或 :%!xxd 命令转换之后，Vim 并不会自动使用 xxd 格式。要应用 xxd 格式的语法加亮，我们需要使用自动命令（可以参考 :help using-xxd），或者手工使用命令 :setf xxd。下图是对上次的二进制文件使用了 xxd 语法加亮的效果：

使用了 xxd 语法加亮的效果

这个格式的语法加亮足够简单，我们就拿它来分析一下。不过，我有个小建议，你在看具体的语法加亮代码前，先花几秒钟的时间看一下图，自己分析一下里面有几种不同的语法加亮效果。

下面我们就来逐步看一下 syntax/xxd.vim 的内容。首先是开头和结尾部分：

" quit when a syntax file was already loaded
if exists("b:current_syntax")
  finish
endif

…

let b:current_syntax = "xxd"

" vim: ts=4

最后一行的模式行，设定了这个文件使用的 tab 宽度。剩余部分基本上算是语法文件的固定格式了，有一个检查缓冲区变量（使用前缀 b:）、防止语法文件重复载入的条件判断，并在结尾设定这个缓冲区变量为语法的名称。

剩余部分可以分为两段。第一段是语法匹配：

syn match xxdAddress            "^[0-9a-f]\+:"      contains=xxdSep
syn match xxdSep    contained   ":"
syn match xxdAscii              "  .\{,16\}\r\=$"hs=s+2 contains=xxdDot
syn match xxdDot    contained   "[.\r]"

这儿定义了 4 种不同的“语法项目”，其中 1、2 和 3、4 还互相有包含（“contains”）和被包含（“contained”）的关系。

xxdAddress。它是地址匹配，所以匹配条件是从行首开始的一个或更多的十六进制字符后面跟一个冒号。

xxdSep。它是分隔符，仅匹配 xxdAddress 中的冒号部分，也算是地址的一部分。

xxdAscii。它是右边的 ASCII 字符部分，条件是两个空格后面跟最多 16 个字符，然后是可选的 CR 字符（\= 和 \? 效果相同），然后必须是一行结束。

xxdDot。它是对“.”和 CR 字符的特殊匹配，可以留意一下上面图里“.”和其他字符的加亮效果的不同之处。同样，这个句点也属于 ASCII 字符部分。

上面的正则表达式都比较简单，唯一之前没出现过的是第 3 个正则表达式后面的 hs=s+2：它的含义是语法加亮的起始位置是模式匹配部分的开始位置再加 2（可查看 :help :syn-pattern-offset），这是在语法加亮文件里的常用特殊语法。

上面的代码可以从 xxd 格式的内容中找出 4 种不同的语法格式。如何展示这些语法，就要看下面的第二段代码了：

" Define the default highlighting.
if !exists("skip_xxd_syntax_inits")

 hi def link xxdAddress Constant
 hi def link xxdSep     Identifier
 hi def link xxdAscii   Statement

endif

外面的条件语句不是惯用法，我们可以忽略。里面重要的是三个 hi def link 语句，拼写完整的话是 highlight default link（可参见帮助 :help :highlight-link）。这三个语句建立了默认的语法加亮链接组，也就是，在用户没有自己在 vimrc 配置文件中使用 highlight link 来修改语法加亮时，默认的语法项目和加亮组之间的关系。目前，地址 xxdAddress 使用常数 Constant 的加亮方式，冒号分隔符 xxdSep 使用标识符 Identifier 的加亮方式，ASCII 文本 xxdAscii 使用语句 Statement 的加亮方式。

那 xxdDot 到哪儿去了呢？答案是，它没有加亮组，因为我们不需要对其进行特殊加亮。虽然 Vim 会认出它使用了特殊的语法格式，在显示上它和中间的十六进制数值一样，没有任何语法加亮效果。

Constant、Identifier、Statement 这些加亮组，又应该以何种方式展示呢？这就是配色方案要做的事情了。如果说语法加亮是逻辑问题的话，那配色方案就是个审美问题。你要个性化的话，就靠配色方案了。

配色方案

类似地，配色方案里包含的也是一些模板语句加上色彩的定义。比如，在配色方案 koehler 里，跟 xxd 相关的核心色彩定义是：

set background=dark
hi Normal      guifg=white  guibg=black
hi Constant       term=underline  cterm=bold ctermfg=magenta  guifg=#ffa0a0
hi Identifier     term=underline   ctermfg=brown  guifg=#40ffff
hi Statement      term=bold  cterm=bold ctermfg=yellow  gui=bold  guifg=#ffff60

首先，这个配色方案设定背景为 dark，深色（允许的另外一个值是 light，浅色背景）。这会调整缺省的颜色组，使得文字色彩在深色背景上显示比较友好。但这不会在终端里真正改变背景（仍要靠下面的背景色设定），因此，如果你在浅色背景的终端里使用这个配色方案，会显得不太友好。有些比较好的配色方案会采用相反的做法，根据目前是深色还是浅色背景，采用不同的配色。

对于“正常”（Normal）的加亮组，这个配色方案采用了最直截了当的前景白、背景黑。可以预见，这个配色会比较醒（cì）目（yǎn）。

对于 Constant 加亮组，这个配色方案就稍微复杂点了，分了单色终端、色彩终端和图形界面的不同配色。古老的单色终端里使用下划线（应该已经没人用吧，所以以后我就忽略这种设定了）；色彩终端下使用粗体和紫色前景；图形界面指定了前景色为 RGB 色彩 #ffa0a0，亮棕色。

Identifier 加亮组也类似，色彩终端下使用棕色前景，图形界面下前景色则是 RGB 色彩 #40ffff，亮青色。

Statement 加亮组在色彩终端和图形界面下都使用粗体，色彩终端使用黄色前景色，图形界面使用前景色是 RGB 色彩 #ffff60，亮黄色。

使用这个配色方案在图形界面和色彩终端下的效果，如下面的截图所示：

图形界面下 koehler 配色方案的效果

色彩终端下 koehler 配色方案的效果

配色方案在终端下的优化

说到这里，我们有必要来讨论一下 Vim 里允许使用的色彩数量。在图形界面 Vim 里，色彩是 Vim 本身调用系统的编程接口来控制的，可以使用 RGB 的所有 16,777,216 种不同颜色。但在终端里，Vim 会受到终端能力的限制，只能根据终端的能力来显示色彩。根据终端的类型，我们可以分为 4 种情况：

第 1 种是最古老的是单色终端，没有颜色，只能使用下划线、粗体等效果。效果定义使用 term=… 的形式。今天，我们应该基本碰不到这样的环境了。

第 2 种是 8/16 色终端，允许使用最基本的八种颜色（黑、红、绿、黄、蓝、紫、青、白），以及这些颜色的较亮变体（即使 8 色终端一般也能在前景色上使用加亮的变体）。我们可以使用 cterm=… 定义粗体等效果（由于兼容性问题，不常用），用 ctermfg=… 和 ctermbg=… 定义前景和背景色，其中可以使用英文色彩名称或序号（见 :help cterm-colors）。鉴于序号在不同的环境里可能是不同的，我们一般使用色彩名称。如果你使用非图形界面终端，可能会遇到这种情况，但这应当也很不常见了吧。

这些颜色虽然是标准的，但很多终端允许用户调整这些颜色，以达到最好的色彩组合效果。比如，下图是 macOS 里终端应用的一个设置界面，其中的“ANSI 颜色”就是用户可以调整的 16 种“标准色”：

macOS 终端应用的文本设置界面

第 3 种是 256 色终端，用户可以选择预先定义的 256 种颜色之一，这在目前的终端里是非常主流的方式了。你可以在网上很方便地找到脚本来输出这些颜色，效果如下图所示：

在 iTerm2 下使用脚本输出的 256 种颜色

要选择这 256 种颜色中的一种，方式不太直观：你需要使用 ctermfg=… 和 ctermbg=…，并直接写出这 256 种颜色之一的编号。

这 256 种颜色都可以算是标准的，它们的标准 RGB 值有明确的定义。头 16 种颜色就是上面的“ANSI 颜色”，在终端里常常可以直接调整，图中也可以看到和前面图里的颜色已经有明显的不同。虽然界面只提供了头 16 种颜色的调整，但为了达到最佳的显示效果，你也可以编程修改这 256 种颜色的调色板。

第 4 种是支持真彩（truecolor）的终端，跟编程修改 256 色的调色板相比，这是更简单的做法。下面是部分比较常见的支持 RGB 真彩的终端（此处是一个更完整的列表）：

GNOME-Terminal（Linux）

iTerm2（macOS）

mintty（Windows）

命令提示符（Windows 10 版本 1703 及以后；在命令提示符里使用 Vim，如果不启用真彩支持，颜色可能完全错误！）

在这些终端里，终端 Vim 就能显示跟图形界面 Vim 同样多的颜色数，因而能达到最佳色彩效果。你仍需手工打开（默认关闭的）Vim 选项 termguicolors。此后，Vim 就会使用你在 guifg 和 guibg 中写的 RGB 色彩，也就是说，把终端当图形界面一样看待（在色彩方面）。

鉴于真彩终端的一个惯例是设置环境变量 COLORTERM 为 truecolor 或 24bit，我们可以在 vimrc 配置文件中进行检查：

if has('termguicolors') &&
      \($COLORTERM == 'truecolor' || $COLORTERM == '24bit')
  set termguicolors
endif

不过，这个检查方式仅限于类 Unix 平台。对于 Windows，Vim 提供了另外一个专门的特性检查项：

if has('vcon')
  set termguicolors
endif

推荐配色方案

上面我们分析的 koehler，算是 Vim 内置的配色方案中比较中规中矩的一个，可用，但不那么好看。如果你想要一个漂亮的配色方案，还是不应该在内置配色方案里寻找。

一个广受好评的 Vim 配色方案是 gruvbox（包管理器中的安装名称是“morhetz/gruvbox”）。它不仅是一个支持深色背景和浅色背景的配色方案，而且还特意确保自己能和 Vim 的一些最流行插件兼容。下面是这个配色方案的示意截图：

浅色背景下的 gruvbox 效果图

深色背景下的 gruvbox 效果图

我觉得 gruvbox 在深色背景下还是挺漂亮的。它的颜色总体偏暖，而你如果喜欢深色背景下较为清冷的色彩，我觉得 jellybeans（包管理器中的安装名称是“nanotech/jellybeans.vim”）还不错：

使用深色背景的 jellybeans 效果图

不过，这两种方案我都是偶尔使用，我使用更多的还是明白（mbbill）设计的 desertEx（包管理器中的安装名称是“mbbill/desertEx”）的一个版本。它的特点是无加亮的普通文字对比度设得比较低，读起来比较轻松（但加亮部分效果仍然比较强烈）。如果你想试试这个方案的话，可以自己安装尝试一下：

我用的 desertEx 效果图（非最新版本）

检查 / 调试配色方案

如果你想自己对配色方案进行调整的话，有一个小工具肯定会非常有用，那就是 vim-scripts/SyntaxAttr.vim。不过，这个插件不会自己添加键映射，需要你在用包管理器安装之后，自己在 vimrc 配置文件中加入类似下面的语句：

nnoremap <Leader>a :call SyntaxAttr()<CR>

这样，我们就能用 \a 来检查光标下面的语法高亮详情了。下面是一个示例：

检查光标下文本的加亮组

从上面可以看到，constexpr 属于 cppStorageClass 语法加亮组（这是在 syntax/cpp.vim 中定义的），并且被链接到了 Type 加亮组。后面的 guifg 和 gui 设定就是 Type 加亮组的内容：使用色彩 tan1（RGB 值为 #ffa54f），特殊效果为粗体（bold）。

在你自己设计、调试语法文件或配色方案时，你会发现这个工具非常有用。

输出加亮效果

作为一个文本编辑器，Vim 只接受文本的复制和粘贴。如果你想要在一个（非 Markdown）文档中展示有语法加亮的代码，Vim 也是可以用来产生这样的代码的——通过 HTML 输出。

Vim 默认就提供了 :TOhtml 命令（参见第 4 讲中讨论的系统内置插件），可以把当前展示的语法加亮效果输出为一个 HTML 文件。你可以根据最终文档的要求，选择合适的深色或浅色配色方案，然后使用该命令来输出 HTML。这个命令默认输出整个文件，你也可以自己在可视模式下选定范围，或者用逗号隔开的行号选定范围，这样 :TOhtml 命令就只会输出选定范围的 HTML 代码。

这个方式灵活是挺灵活，但不能直接把带色彩加亮的文本贴到文档里去，终究还是不太方便。令人欣慰的是，早就有人找到了在各个平台上把 HTML 代码转成剪贴板富文本的方法。我最近对一个 Vim 插件 vim-copy-as-rtf 作了点改造，使其可以在我们现在讲的三大主流平台（macOS、Linux 和 Windows）上都可以直接复制出带语法加亮的代码。在 macOS 和 Windows 上，没有特别的配置要求；在 Linux 桌面环境下，我们要求系统必须装有 xclip 工具。这样，我们只需要在使用 TOhtml 的地方，把命令改成 CopyRTF 就能把加亮的代码复制到系统的剪贴板中。

内容小结

好了，我们现在来总结一下今天学到的内容：

Vim 使用正则表达式来匹配代码，把代码分到不同的“语法项目”里。

语法项目可以跟加亮组进行链接，加亮组可以针对终端和图形界面定义不同的显示方式。

主流的终端可以显示 256 色，某些终端已经可以跟图形界面一样显示 RGB 真彩。

我推荐了 gruvbox、jellybeans 和 desertEx 三个配色方案；当然，你也可以自己在网上找其他好的配色方案。

SyntaxAttr.vim 可以显示语法加亮组的细节，可以用来帮助调试语法加亮和配色方案。

Vim 的加亮效果可以输出成 HTML 文件，也可以复制到剪贴板中成为带语法加亮的富文本，方便在办公文档中使用。

本讲我们的配置文件有一些改动，对应的标签是 l12-unix 和 l12-windows。

课后练习

想一想，如果希望颜色在 8/16 色终端下和 256 色终端下都有效，并且在 256 色终端下使用 256 色的配置，该怎么做？

在自己尝试解决这个问题之后，可以看一下 jellybeans 里面的颜色定义，了解它是如何解决在不同终端下面的颜色一致性问题的。

如果有任何问题，欢迎留言和我交流。

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

13｜YouCompleteMe：Vim 里的自动完成

你好，我是吴咏炜。

在集成开发环境里，自动完成是一个非常重要的功能。可是 Vim 并不能真正理解你输入的代码，因此它自身无法提供自动完成的功能。不过，Vim 仍然提供了一些接口，允许第三方的软件实现这样的功能，并和 Vim 自身进行集成。YouCompleteMe（简称 YCM）就是这样的一个第三方软件，今天，我就为你详细介绍一下它。

YCM 对 C++ 程序员最为适合，它可以提供其他工具实现不了的功能。而且，它也适用于很多其他语言，包括 C 家族的各种语言和其他常用的语言，如 Python、Java 和 Go 等。即使在 YCM 不直接支持你使用的语言的时候，它仍然能通过标识符完成功能提供比没有 YCM（和其他语言支持插件）时更好的编辑体验。因此，我推荐你使用这个插件。

YouCompleteMe

功能简介

首先我来介绍一下 YCM 的基本功能吧。根据它的主页（我的翻译）：

YouCompleteMe 是一个快速、即输即查、模糊搜索的 Vim 代码完成引擎。它实际上有好几个完成引擎：

一个基于标识符的引擎，可以在任何编程语言中工作

一个强大的基于 clangd 的引擎，可以为 C/C++/Objective-C/Objective-C++/CUDA（C 家族语言）提供原生的语义代码完成

一个基于 Jedi 的完成引擎，可以支持 Python 2 和 3

一个基于 OmniSharp-Roslyn 的完成引擎，用来支持 C#

一个基于 Gopls 的完成引擎，支持 Go

一个基于 TSServer 的完成引擎，支持 JavaScript 和 TypeScript

一个基于 rls 的完成引擎，支持 Rust

一个基于 jdt.ls 的完成引擎，支持 Java

一个通用的语言服务器协议（LSP）实现，用来支持任何其他有 LSP 服务器的语言

还有一个基于 omnifunc 的完成器，使用 Vim 的全能补全（omnicomplete）系统提供的数据来为很多其他语言提供语义完成

其实，Vim 里的自动完成插件并不止这一个，但 YCM 是比较成熟也比较全面的。虽说它的安装配置有一定的复杂性，但比起另外一些要求你独立安装、配置语言服务器的方案，它至少能一次性搞定插件和你需要的语言支持，所以反而算是简单的了。我最近的主要开发语言是 C、C++、Python 和 Vim 脚本，因此这也算是个很完美的匹配了。

下面是一个简单的示例，展示了 YCM 的效果。

YCM 的 C++ 示例

总体上，你只要在 YCM 给你提示的时候，敲 <Tab> 来选择合适的选项，然后继续往下输入就行。由于 YCM 使用模糊匹配，你只要输入你希望的标识符中的每一段中的若干字符，就可以快速把候选项减到你要的内容敲一两下 <Tab> 就能出来。事实上，我后来发现，在 std:: 后只要输入 mu 就足以让 make_unique 成为第一选择了。

不过，这里面最让我吃惊的还是，clangd 引擎居然能在我只提供部分头文件的情况下（完全不提供是不行的），自动帮我插入正确的头文件并保持其字母序排列。这个功能我以前还真还没有见过！

安装

Ubuntu 下的 apt 安装

如果你使用一个较新的 Linux 发布版，有可能系统本身已经自带了 YCM。虽然这个版本多半会有点老，但对于有些人来说，可能也够用了。毕竟，Linux 下的包安装确实方便。我们就先以 Ubuntu 为例，来介绍 Linux 包管理器下的安装过程。

首先，我们需要使用 apt 命令来安装 YCM，命令是：

sudo apt install vim-youcompleteme

这步成功之后，YCM 就已经被安装到了你的系统上。不过，在你个人的 Vim 配置里，仍然还没有启用 YCM。要启用的话，可以输入下面的命令：

vim-addon-manager install youcompleteme

这个命令之后，你会在你的 ~/.vim/plugin 目录下看到 youcompleteme.vim 的符号链接。这样，安装就算完成了。

手动安装

如果你的系统不直接提供 YCM，或者你想要使用最新版本的 YCM，那你就需要手工编译安装了。安装之前，你需要确保你的系统上有 CMake、Python 3 和平台主流的 C++ 编译器，即 Linux 上的 GCC，macOS 上的 Clang，及 Windows 上的 MSVC。如果要安装其他语言（如 Java 和 Go）的支持，也同样要准备好相应语言的环境，这些在 YCM 的主页上有介绍，我就先不多说了。

因为 YCM 是一个需要编译组件的插件，所以我不建议你用 Vim 的包管理器来安装，那样会出什么错都搞不清楚。大致安装过程是：

选择安装目录

签出 YCM

根据你需要使用的语言使用合适的选项，来进行编译安装

下面，我们就快速地过一下。

首先，我们需要给 YCM 一个独立的安装目录。这个目录应该在 pack 下面，但不要放在包管理器使用的目录下，以免发生冲突。我的选择是“我的”，my。因为希望 YCM 直接启动，所以最后需要放到这个目录的 start 子目录下。换句话说，Unix 上的 ~/.vim/pack/my/start，Windows 上的 ~\vimfiles\pack\my\start。

然后，我们就应当在这个目录下签出 YCM。可以在进到这个子目录里面后，使用下面的命令（Windows 下面去掉“\”全部写一行，或者把“\”换成“^”）：

git clone --recurse-submodules \
          --shallow-submodules \
    https://github.com/ycm-core/YouCompleteMe.git

最后就是编译安装了。主要工作由 install.py 来完成，但如果我们不提供额外的选项，YCM 不会安装上面说的那些特定语言的语义完成引擎。我们需要显式地提供相应语言的选项：

--clang-completer，基于 libclang 的老 C 族语言引擎

--clangd-completer，基于 clangd 的新实验 C 族语言引擎

--cs-completer，C# 引擎

--go-completer，Go 引擎

--rust-completer，Rust 引擎

--java-completer，Java 引擎

--ts-completer，JavaScript 和 TypeScript 引擎

--all，除 clangd 外的上述索引引擎

关于 clangd，我多说一句。虽然这个引擎被标为实验状态，但它的易用性和功能确实比老的引擎有了巨大的提升。同样是上面的代码，如果用老的 libclang 引擎的话，效果是这样的：

使用 libclang 的自动完成示例

我们可以看到：

老版本不会添加 #include 结束的 > 或 "

老版本不会自动添加头文件

老版本不会提供函数原型提示

老版本不会在输入中时刻提醒当前有错误（这倒不算是件坏事）

所以，如果你编译和使用 clangd 支持没有问题，那就用它吧。对我来说，使用 libclang 引擎可能有两个理由：

clangd 支持编译不过（我遇到过）

机器配置低，clangd 太慢了（我的机器上能感到性能差异，但 clangd 的响应速度完全可以接受）

你可以同时安装这两个引擎，然后通过你的 vimrc 配置文件来选择使用哪一个，使用 let g:ycm_use_clangd = 0 就是使用老引擎，这个值设为 1 或者干脆不设，则是使用新引擎。

另外一个要提醒你的地方是，编译环境应尽可能干净，不要暴露出自己用的第三方库的路径。我就碰到过因为环境变量里设了 Boost 库的包含路径，从而导致 YCM 编译出错的情况。YCM 自己已经包含了所需的依赖库，系统的和用户自己安装的类似库如果版本不合适的话，反而会对 YCM 造成干扰。

（此外，也告诉你一下我编译 clangd 时遇到的失败情况，也供你参考一下。我的原因是自己编译了 Python 3，由于系统上缺了一些开发包，导致 Python 功能不完整，到运行 YCM 的安装脚本时才暴露出来。我很高兴我后来花点时间解决了问题，因为 clangd 的功能真的强大很多。）

配置

项目配置

像我上面的简单例子，YCM 是可以不需要配置就能直接工作的。但如果环境稍微复杂一点，C/C++ 程序就可能会出现识别错误。原因通常是以下三种：

头文件没找到，可能是因为项目内部路径比较复杂，也可能是因为编译器的头文件不是在 Clang 查找的默认位置下面

项目需要特别的宏定义

项目需要特定的 C 或 C++ 标准，或特定的编译器选项

这些情况如果出现的话，你需要让 YCM 了解项目的相应信息。

YCM 在 clangd 下的推荐做法是在源代码或其某个父目录下放一个 CMake 输出的 compile_commands.json 文件（可在 cmake 命令行上加上 -DCMAKE_EXPORT_COMPILE_COMMANDS=1 来产生此文件）。这种方式最为通用和严格，因为 CMake 输出的这个编译命令文件里包含每一个源文件的编译命令，因此只要你的 CMake 配置是正确的，YCM 通常就能正确识别，哪怕你每个文件的编译选项不同都没有关系。它可以给 YCM 提供完整的项目编译信息，使得查找一个符号的引用成为可能。如果你的工程里，这个文件产生在一个 build 目录下的话，别忘了你需要在项目的根目录下执行类似下面的命令：

ln -s build/compile_commands.json .

使用 compile_commands.json 也有缺点。CMake 在这个编译命令文件里放置的是绝对路径，因此，把源代码放在一个共享位置供不同的系统使用就会有麻烦。此外，只有少数 CMake 产生器支持 CMAKE_EXPORT_COMPILE_COMMANDS，特别是，Windows 上默认的 Visual Studio 产生器不支持输出这个文件。抛开这些问题，如果项目比较大的话，clangd 的“编译”会消耗 CPU 和内存，也是一个可能的负面因素。但我们同时要记住，这种开销是让 YCM 能看到整个项目而不只是单个文件的代价。

如果因为某种问题你决定不使用编译命令文件这一方法，那 YCM 的经典做法是在文件所在目录或其父目录下寻找一个名字叫 .ycm_extra_conf.py 的文件。找到之后，它就会弹出一个提醒，提示用户是不是要载入这个文件，运行其中的代码来得到需要的配置信息。

YCM 的配置载入提示

虽然这个恼人的提示可以关掉，但这实际上会带来潜在的安全问题，毕竟 YCM 是会运行其中的 Python 代码的。此外，写这个文件也算是件麻烦事吧，尤其对不熟悉 Python 的人而言。

在 2014—2015 年期间，有人维护了一个叫 ycmconf 的插件，以一种我喜欢的方式解决了这个问题。不过，今天再直接用这个插件，就有点问题了。所以我在 GitHub 上复刻了这个插件，并进行了更新。如果你使用 YCM 进行 C 系语言的自动完成，那我推荐你安装 adah1972/ycmconf 这个插件。

这个插件支持两种简单的方式来配置 C/C++ 的自动识别：

使用 CMake 输出的 compile_commands.json 文件（由于 YCM 目前已经直接支持该文件，这只对较老版本的 YCM 有意义）

使用上一讲（拓展 2）里用的 .clang_complete 配置文件

目前我当然是以第 2 种方式来使用这个插件了：手写一个 .clang_complete 很简单，非常适合临时写的小程序。但它的问题是，这个文件对整个目录有效。所以如果你在其中写了像 -std=c++14 这样的选项，选择某一特定 C++ 标准，那这个选项对于 C 文件就是错的了，YCM 就会抱怨。不过解决起来也很容易，像上面说的情况，让 C 文件单独占一个子目录或者平行目录都可以消除此问题。

不管是哪种方法，你都需要确保配置文件在源代码的目录下或其父目录下。YCM 和 ycmconf 的搜索规则都是从源代码的所在位置往上找有没有满足文件名约定的文件，并在找到第一个时终止。

全局配置

YCM 有很多命令，但它默认只对少量的功能进行了键映射，其中最重要的就是 <Tab> 了。在使用中，我觉得自动修正和跳转功能值得单独进行一下键映射：

nnoremap <Leader>fi :YcmCompleter FixIt<CR>
nnoremap <Leader>gt :YcmCompleter GoTo<CR>
nnoremap <Leader>gd :YcmCompleter GoToDefinition<CR>
nnoremap <Leader>gh :YcmCompleter GoToDeclaration<CR>
nnoremap <Leader>gr :YcmCompleter GoToReferences<CR>

自动修正功能可以参考下图的演示。

YCM 自动修正功能的演示

自动修正的范围很广，小到修正一个拼写错误或者漏写名空间这样的问题，大到提供安全方面或代码风格现代化的调整（它可以利用你的 Clang-Tidy 配置来向你报告错误和提供修正建议）。有了它，编码工作真的轻松了许多。

我们这儿有四种跳转：

GoTo，无脑跳转，最常用的就是这个功能，如果能跳转到定义，就跳转到定义，否则就跳转到声明。

GoToDefinition，顾名思义，就是跳转到定义。

GoToDeclaration，专门跳转到声明。

GoToReferences，可以用来查出一个符号被引用的地方（libclang 引擎不支持该命令）。

注意，对 C 族语言来说，只有让 clangd 看到你的项目的 compile_commands.json 文件，才能使用 GoToReferences 这个命令查找整个项目里符号被引用的地方；否则，你只能查出当前 Vim 里可见的引用，而非整个项目。

YCM 有很多配置参数，有些在默认状态下工作得不是很好。我通常会配置下面这些：

let g:ycm_auto_hover = ''
let g:ycm_complete_in_comments = 1
let g:ycm_filetype_whitelist = {
      \ 'c': 1,
      \ 'cpp': 1,
      \ 'python': 1,
      \ 'vim': 1,
      \ 'sh': 1,
      \ 'zsh': 1,
      \ }
let g:ycm_goto_buffer_command = 'split-or-existing-window'
let g:ycm_key_invoke_completion = '<C-Z>'

第一项 ycm_auto_hover 用来禁用光标在一个符号上长期停留出现的自动文档提示。未禁用时的效果如下图：

YCM 的自动浮动提示

这个自动提示不能说一点用都没有，但它很容易成为写代码时的干扰，所以我还是把它禁用了。

第二项 ycm_complete_in_comments 表示我希望在写注释的时候也能启用自动完成——毕竟注释里通常也要写代码里的变量、函数名什么的。

第三项 ycm_filetype_whitelist 用来仅对白名单列表里的文件类型才启用 YCM。没有这一项，YCM 在打开一些特殊类型的文件时可能会报错，有时候也会导致打开的延迟。我就明确一下，让它只在我常用的源代码类型里才启用。

第四项 ycm_goto_buffer_command 用来告诉 YCM，当跳转的目的文件尚未打开时，用分割窗口的方式打开新文件；如果已经打开则跳转到相应的窗口。其他的可能值是 'same-buffer'，在同一个缓冲区的位置打开（除非这个位置因为文件修改的原因不能被替换），及 'split'，除非跳转目的在同一个文件，永远在新分割的窗口打开。

第五项 ycm_key_invoke_completion 用来定义手工启用语义完成的按键。在你输入时，YCM 会自动尝试标识符匹配，而当你输入 .、->、:: 或这个按键时，YCM 则会启用语义完成，来给出当前上下文中允许出现的符号。这个按键默认是 <C-Space>，在某些操作系统上是不能用的（如 Mac 和老的 Windows），所以我改成了 <C-Z>。你也可以选择你自己喜欢的按键（但要注意映射冲突问题：Vim 里在插入模式下的可用键不多，事实上只有在终端下容易出问题的 <C-S> 和 <C-Z> 在 Vim 里没有默认功能）。

使用

说了这么多，实际上 YCM 大部分使用方法我也已经提到了。它基本上只要你使用 <Tab> 就能使用，你如果不理睬它的提示，它也不会对你造成什么干扰（我遇到过一些 Vim 的插件，虽然能提供些有用的提示，但是会侵入式地影响正常输入，那就只能删除 / 禁用了）。其他最重要的功能，我们也已经进行了按键映射，上面也都有了初步的描述。

还有一个可能有用的命令，不能通过按键映射，那就是重命名的重构。这个命令需要你把光标移到要修改的符号上，然后输入的命令里要有新的名称。比如，如果你要把一个 foo 符号重命名成 bar，需要把光标移动 foo 上面，然后输入：

:YcmCompleter RefactorRename bar

命令虽然略长，但你一样可以用 <Tab> 来自动完成，所以你多打不了几个字符的。

YCM 默认只在屏幕底部显示当前行的问题，并且显示很可能被截断。要看到所有的代码问题，可以使用命令 :YcmDiags。

此外 YCM 还有一些调试命令，一般不需要使用，我这边就不介绍了。你可以自己在帮助文档里查看。

RTags（选学）

因为有段时间我只能用 libclang 引擎，不能查找符号的引用，因此我安装了另外一个开源工具，RTags，来弥补这一缺憾。在使用 clangd 的情况下，RTags 已经不那么必要了。它仍然提供了一些特别的功能，并且 Linux 发布版里可能仍只提供了 libclang 引擎的 YCM，因此我就把这部分作为选学提供了，相当于本讲内部的一个小加餐。爱折腾并且使用非 Windows 环境（RTags 尚不支持 Windows）的小伙伴们可以把这部分读完，其他人就跳到内容小结吧。

安装

这次我就只讲自己构建安装的大概过程了。如果你的平台支持二进制安装，相信你应该可以自己搞定了。

首先我们要安装 RTags 本身。安装前，我们需要确认所在的平台有 CMake、C++ 编译器和 libclang 的开发包。这些都有了之后，我们选一个目录，执行下面的命令就可以编译安装了：

git clone --recursive https://github.com/Andersbakken/rtags.git
cd rtags
mkdir build
cd build
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -DCMAKE_BUILD_TYPE=Release ..
make -j4
sudo make install

在这些命令都正常执行之后，你就已经把 RTags 的命令安装到了 /usr/local 下面。

随后我们安装 RTags 和 Vim 集成的插件。这个比较简单，用你的包管理器安装 lyuts/vim-rtags 即可。在配置文件里，我通常只做一处调整：

let g:rtagsUseLocationList = 0

这是因为 vim-rtags 默认使用位置列表（location list）而不是快速修复窗口。我们之前没有介绍过位置列表，我就快速引用一下文档：

位置列表是一个窗口局部的快速修复列表。由 :lvimgrep、:lgrep、:lhelpgrep、:lmake 等命令产生，它们生成位置列表而不是对应的 :vimgrep、:grep、:helpgrep、:make 生成的快速修复列表。

位置列表和窗口相关联，而每个窗口都要单独的位置列表。一个位置列表只能和一个窗口相关联。位置列表和快速修复列表相互独立。

我通常不怎么使用位置列表，主要是为了简单，可以使用固定的快捷键。

运行和项目配置

RTags 是一个客户端 / 服务器端架构的程序。但它不是用 TCP/IP，而是 Unix 域套接字，一个用户只能运行一个服务器，可以支持多个客户端。启动服务器端的命令非常简单，就是：

rdm &

我这儿用了 &，让 rdm 在后台运行，但输出仍能直接从终端上看到。在我们刚开始学习使用 RTags 时，我们仍需多多监控 rdm 的输出。

在某个项目中启用 RTags，最简单的方式就是使用 CMake 输出的 compile_commands.json 文件。我们只需要在这个目录下执行以下命令：

rc -J .

随后我们就能看到运行 rdm 的那个窗口屏幕哗哗地翻滚，忙着“编译”代码。等到 rdm 忙完了，项目索引就算完成了。而当你修改代码时，rdm 会看到你修改，然后就会自动编译相关的文件，保持索引为最新。

有没有注意到我上面编译 RTags 的命令已经生成了 compile_commands.json 文件？所以，如果你没有现成的其他 CMake 项目，你可以用这个项目本身来进行搜索。

使用

如果我们使用默认的 vim-rtags 键映射的话，我们只需要把光标移到一个符号上面，然后输入 \rf（理解为 find references）即可。下面是一个示例：

Vim-rtags 的使用示例

这个结果出来的速度比用 :grep 可快多了！

它还有很多其他命令，可以用来查找定义、查找父类、查找子类、显示调用树，等等。有些功能在 YCM 里并没有对应物，这也是 RTags 的价值了。你可以在 vim-rtags 的主页上查看学习所有这些命令及其快捷键。

内容小结

在本讲中，我们主要介绍了 YouCompleteMe 这个重量级插件，包括其安装和配置。我们可以看到，在插件的帮助下，我们可以获得不输于集成开发环境的自动完成体验，同时，仍然享受 Vim 的快速启动和强大编辑功能。最后我们花了一点点时间介绍了 RTags 工具和 vim-rtags 插件，它在你写 C 族语言而不能使用 clangd 引擎时会特别有帮助。

YCM 的参数和键映射我写到了示例配置文件里，对应的标签是 l13-unix 和 l13-windows。

课后练习

今天学完之后的主要任务，当然就是把 YouCompleteMe 装起来、配置好了。它的使用反而是相当简单的，大部分情况下使用 <Tab> 就行。

至于 RTags，Unix 下的 C++ 程序员们可以根据自己的兴趣，决定是否捣腾一下。这个工具还是有点可玩性的。

如果遇到什么问题，欢迎留言和我讨论。我们下一讲再见！

14｜Vim 脚本简介：开始你的深度定制

你好，我是吴咏炜。

学到今天，我们已经看到了很多的 Vim 脚本，只是还没有正式地把它作为一门语言来介绍。今天，我就正式向你介绍把 Vim 的功能粘合到一起的语言——Vim 脚本（Vim script）。掌握 Vim 脚本的基本语法之后，你就可以得心应手地定制你的 Vim 环境啦。

语法概要

首先，我们需要知道，通过命令行模式执行的命令就是 Vim 脚本。它是一种图灵完全的脚本语言：图灵完全，说明它的功能够强大，理论上可以完成任何计算任务；脚本语言，说明它不需要编译，可以直接通过解释方式来执行。

当然，这并没有说出 Vim 脚本的真正特点。下面，我们就通过各个不同的角度，进行了解，把 Vim 脚本这头“大象”的基本形状完整地摸出来。

在这一讲里，我们改变一下惯例，除非明确说“正常模式命令”，否则用代码方式显示的都是脚本文件里的代码或者命令行模式命令，也就是说，它们前面都不会加 :。毕竟我们这一讲介绍的全是 Vim 脚本，而不是正常模式的快捷操作。

打印输出和字符串

学习任何一门语言，我们常常以“Hello world!”开始。对于 Vim 脚本，我们不妨也这样——毕竟，打印是一种重要的调试方式，尤其对于没有专门调试器的脚本语言来说。

Vim 脚本的“Hello world!”是下面这样的：

echo 'Hello world!'

echo 是 Vim 用来显示信息的内置命令，而 'Hello world!' 是一个字符串字面量。Vim 里也可以使用 " 来引起一个字符串。' 和 " 的区别和在 shell 里比较相似，前者里面不允许有任何转义字符，而后者则可以使用常见的转义字符序列，如 \n 和 \u.... 等。和 shell 不同的是，我们可以在 ' 括起的字符里把 ' 重复一次来得到这个字符本身，即 'It''s' 相当于 "It's"。不过，在这个例子里，显然还是后者更清晰了。

因为 " 还有开始注释的作用，一般情况下我推荐在 Vim 脚本里使用 '，除非你需要转义字符序列或者需要把 ' 本身放到字符串里。

字符串可以用 . 运算符来拼接。由于字典访问也可以用 . ，为了避免歧义，Bram 推荐开发者在新的 Vim 脚本中使用 .. 来拼接。但要注意，这个写法在 Vim 7 及之前的版本里不支持。我目前仍暂时使用 . 进行字符串拼接，并和其他大部分运算符一样，前后空一格。这样跟不空格的字典用法比起来，差异就相当明显了。

除了 echo，Vim 还可以用 echomsg（缩写 echom）命令，来显示一条消息。跟 echo 不同的是，这条消息不仅会显示在屏幕上，还会保留在消息历史里，可以在之后用 message 命令查看。

变量

跟大部分语言一样，Vim 脚本里有变量。变量可以用 let 命令来赋值，如下所示：

let answer = 42

然后你当然就可以使用 answer 这个变量了，如：

echo 'The meaning of life, the universe and everything is ' . answer

Vim 的变量可以手工取消，需要的命令是 unlet。在你写了 unlet answer 之后，你就不能再读取 answer 这个变量了。

数字

上面的赋值语句用到了整数。Vim 脚本里的数字支持整数和浮点数，在大部分平台上，两者都是 64 位的有符号数字类型，基本对应于大部分 C 语言环境里的 int64_t 和 double。表示方式也和 C 里面差不多：整数可以使用 0（八进制）、0b（二进制）和 0x（十六进制）前缀；浮点数含小数点（不可省略），可选使用科学计数法。

复杂数据结构

Vim 脚本内置支持的复杂数据结构是列表（list）和字典（dictionary）。这两者都和 Python 里的对应数据结构一样。对于 C++ 的程序员来说，列表基本上就是数组 /array/vector，但大小可变，而且可以直接使用方括号表达式来初始化，如：

let primes = [2, 3, 5, 7, 11, 13, 17, 19]

然后你可以用下标访问，比如用 primes[0] 就可以得到 2。

字典基本上就是 map，可以使用花括号来初始化，如：

let int_squares = {
      \0: 0,
      \1: 1,
      \2: 4,
      \3: 9,
      \4: 16,
      \}

键会自动转换成字符串，而值会保留其类型。上面也用到了 Vim 脚本的续行——下一行的第一个非空白字符如果是 \，则表示这一行跟上一行在逻辑上是同一行，这一点和大部分其他语言是不同的。

访问字典里的某一个元素可以用方括号（跟大部分语言一样），如 int_squares['2']；或使用 .，如 int_squares.2。

表达式

跟大部分编程语言类似，Vim 脚本的表达式里可以使用括号，可以调用函数（形如 func(…)），支持加（+）、减（-）、乘（*）、除（/）和取模（%），支持逻辑操作（&&、|| 和 !），还支持三元条件表达式（a ? b : c）。前面我们已经学过，可以使用 [] 访问列表成员，可以使用 [] 或 . 访问字典的成员，也可以使用 . 或 .. 进行字符串拼接。== 和 != 运算符对所有类型都有效，而 <、>= 等运算符对整数、浮点数和字符串都有效。

对于文本处理，常见的情况是我们使用 =~ 和 !~ 进行正则表达式匹配，前者表示匹配的判断，后者表示不匹配的判断。比较操作符可以后面添加 # 或 ? 来强制进行大小写敏感或不敏感的匹配（缺省受 Vim 选项 ignorecase 影响）。表达式的左侧是待匹配的字符串，右侧则是用来匹配的正则表达式。

注意表达式不是一个合法的 Vim 命令或脚本语句。在表达式的左侧，需要有 echo 这样的命令。如果你只想调用一个函数，而不需要使用其返回的结果，则应使用 call func(…) 这样的写法。

此外，我们在插入模式和命令行模式下都可以使用按键 <C-R>=（两个键）后面跟一个表达式来使用表达式的结果。在替换命令中，我们在 \= 后面也同样可以跟一个表达式，来表示使用该表达式的结果。比如，下面的命令可以在当前编辑文件的每一行前面插入行号和空格：

:%s/^/\=line('.') . ' '/

line 是 Vim 的一个内置函数，line('.') 表示“当前”行的行号，剩下部分你应该直接就明白了吧？

控制结构

作为一门完整的编程语言，标准的控制结构当然也少不了。Vim 支持标准的 if、while 和 for 语句。语法上，Vim 的写法有点老派，跟当前的主流语言不太一样，每种结构都要用一个对应的 endif、endwhile 和 endfor 来结束，如下面所示：

" 简单条件语句
if 表达式
  语句
endif

" 有 else 分支的条件语句
if 表达式
  语句
else
  语句
endif

" 更复杂的条件语句
if 表达式
  语句
elseif 表达式
  语句
else
  语句
endif

" 循环语句
while 表达式
  语句
endwhile

在 while 和 for 循环语句里，你可以使用 break 来退出循环，也可以使用 continue 来跳过循环体内的其他语句。作为一个程序员，理解它们肯定没有任何困难。

Vim 脚本的 for 语句跟 Python 非常相似，形式是：

for var in object
  这儿可以使用 var
endfor

表示遍历 object（通常是个列表）对象里面的所有元素。

哦，跟 Python 一样，Vim 脚本也没有 switch/case 语句。

函数和匿名函数

为了方便开发，函数肯定也是少不了的。Vim 脚本里定义函数使用下面的语法：

function 函数名(参数1, 参数2, ...)
  函数内容
endfunction

Vim 里用户自定义函数必须首字母大写（和内置函数相区别），或者使用 s: 表示该函数只在当前脚本文件有效。... 可以出现在参数列表的结尾，表示可以传递额外的无名参数。使用有名字的参数时，你需要加上 a: 前缀。要访问额外参数，则需要使用 a:1、a:2 这样的形式。特殊名字 a:0 表示额外参数的数量，a:000 表示把额外参数当成列表来使用，因而 a:000[0] 就相当于 a:1。

在函数里面，跟大部分语言一样，你可以使用 return 命令返回一个结果，或提前结束函数的执行。

Vim 脚本里允许匿名函数，形式是 {逗号分隔开的参数 -> 表达式}。如果你对函数式编程完全没有概念，你可以跳过匿名函数。如果你喜欢函数式编程，那你应该会很欣喜地看到，在 Vim 脚本里可以使用类似下面的语句：

echo map(range(1, 5), {idx, val -> val * val})

结果是 [1, 4, 9, 16, 25]。跟常见的 map 函数不同，Vim 会传过去两个参数，分别是列表索引和值；同时，它会修改列表的内容。不想修改的话，要把列表复制一份，如 copy(mylist)。

Vim 特性

上面描述的只是一般性的编程语言语法，但 Vim 脚本如果只当作通用编程语言来用的话，就没啥意义了。我们使用 Vim 脚本，肯定是为了和 Vim 进行交互。下面我们就来仔细检查一下 Vim 脚本里的 Vim 特性。

变量的前缀

我们上面已经提到了变量的 a: 前缀。变量的前缀实际上有更多，通用编程概念上很容易理解的是下面四个：

a: 表示这个变量是函数参数，只能在函数内使用。

g: 表示这个变量是全局变量，可以在任何地方访问。

l: 表示这个变量是本地变量，但一般这个前缀不需要使用，除非你跟系统的某个名字发生了冲突。

s: 表示这个变量（或函数，它也能用在函数上）只能用于当前脚本，有点像 C 里面的 static 变量和函数，只在当前脚本文件有效，因而不会影响其他脚本文件里定义的有冲突的名字。

一般编程语言里没有的，是下面这些前缀：

b: 表示这个变量是当前缓冲区的，不同的缓冲区可以有同名的 b: 变量。比如，在 Vim 里，b:current_syntax 这个变量表示当前缓冲区使用的语法名字。

w: 表示这个变量是当前窗口的，不同的窗口可以有同名的 w: 变量。

t: 表示这个变量是当前标签页的，不同的标签页可以有同名的 t: 变量。

v: 表示这个变量是特殊的 Vim 内置变量，如 v:version 是 Vim 的版本号，等等（详见 :help v:var）。

还有下面这些前缀，可以让我们像使用变量一样使用环境变量和 Vim 选项：

$ 表示紧接着的名字是一个环境变量。注意，一些环境变量是由 Vim 自己设置的，如 $VIMRUNTIME。

& 表示紧接着的名字是一个选项，比如， echo &filetype 和 set filetype? 效果相似，都能用来显示当前缓冲区的文件类型。

&g: 表示访问一个选项的全局（global）值。对于有本地值的选项，如 tabstop，我们用 &tabstop 直接读到的是本地值了，要访问全局值就必须使用 &g:tabstop。

&l: 表示访问一个选项的本地（local）值。对于有本地值的选项，如 tabstop，我们用 &tabstop 直接读到的已经是本地值了，但修改则和 set 一样，同时修改本地值和全局值。使用 &l: 前缀可以允许我们仅修改本地值，像 setlocal 命令一样。

你可能要问，什么时候我们会需要用变量形式来访问选项，而不是使用 set、setlocal 这样的命令呢？答案是，当我们需要计算出选项值的时候。set filetype=cpp 基本上和 let &filetype = 'cpp' 等效，我们需要注意到后者里面 cpp 是个字符串，可以是通过某种方式算出来的。光使用 set，就不方便做到这样的灵活性了。

重要命令

Vim 里有很多命令，很多我们已经介绍过，或者直接在 vimrc 配置文件里使用了。这节里我们会介绍跟 Vim 脚本相关性比较大的一些命令。

首先是 execute（缩写 exe），它能用来把后面跟的字符串当成命令来解释。跟上一节使用选项还是 & 变量一样，这样做可以增加脚本的灵活性。除此之外，它还有两种常见特殊用法：

在使用键盘映射等场合、需要在一行里放多个命令时，一般可以使用 | 来分隔，但某些命令会把 | 当成命令的一部分（如 !、command、nmap 和用户自定义命令），这种时候就可以使用 execute 把这样的命令包起来，如：exe '!ls' | echo 'See file list above'。

normal 命令把后面跟的字符直接当成正常模式命令解释，但如果其中包含有特殊字符时就不方便了。这时可以用 execute 命令，然后在 " 里可以使用转义字符。我们上面讲字符串时没说的是，按键也可以这样转义，比如，"\<C-W>" 就代表 Ctrl-W 这个按键。所以，如果你想在脚本中控制切换到下一个窗口，可以写成：exe "normal \<C-W>w"。

然后，我要介绍一下 source（缩写 so）命令。它用来载入一个 Vim 脚本文件，并执行其中的内容。我们已经多次在 vimrc 配置文件中使用它来载入系统提供的 Vim 脚本了，如：

source $VIMRUNTIME/vimrc_example.vim
…
command! PackUpdate packadd minpac | source $MYVIMRC | call minpac#update('', {'do': 'call minpac#status()'})
…

这里要注意的地方是，要允许一个文件被 source 多次，是需要一些特殊处理的。我目前给出的 vimrc 配置文件由于需要被载入多次，进行了下面的特殊处理：

清除缺省自动命令组里当前的所有命令，以免定义的自动命令被执行超过一次

使用 command! 来定义命令，避免重复命令定义的错误

使用 function! 来定义函数，避免重复函数定义的错误

没有手工设置 set nocompatible，因为该设置可能会有较多的副作用（在 defaults.vim 里会确保只设置该选项一次）

上面我已经展示了一个 command 命令的例子。这个命令允许我们自定义 Vim 的命令，并允许用户来定制自动完成之类的效果（详见 :help user-commands）。注意这个命令的定义要写在一行里，所以如果命令很长，或者中间出现会吞掉 | 的命令的话，我们就会需要用上 execute 命令了。

最后，我再说明一下我们用过的 map 系列键映射命令（详见 :help key-mapping）。这些命令的主干是 map，然后前面可以插入 nore 表示键映射的结果不再重新映射，最前面用 n、v、i 等字母表示适用的 Vim 模式。在绝大部分情况下，我们都会使用带 nore 这种方式，表示结果不再进行映射（排除偶尔偷懒的情况）。但是，如果我们的 map 命令的右侧用到了已有的（如某个插件带来的）键映射，我们就必须使用没有 nore 的版本了。

事件

和用户主动发起的命令相对应，Vim 里的自动处理依赖于 Vim 里的事件。迄今为止，我们已经遇到了下面这些事件：

BufNewFile 事件在创建一个新文件时触发

BufRead（跟 BufReadPost 相同）事件在读入一个文件后触发

BufWritePost 事件在把整个缓冲区写回到文件之后触发

FileType 事件在设置文件类型（filetype 选项）时被触发

Vim 里的事件还有很多（详见 :help autocmd-events-abc），我们就不一一介绍了。上面这些是我们最常用的，你应该了解它们的意义。

内置函数

Vim 里内置了很多函数（列表见 :help function-list），可以实现编程语言所需要的基本功能。我们目前用得比较多的是下面这两个：

exists 用来检测某一符号（变量、函数等）是否已经存在。在 Vim 脚本里最常见的用途是检测某一变量是否已经被定义。

has 用来检测某一 Vim 特性（列表见 :help feature-list）是否存在。帮助文档里已经描述得很清楚，我就不详细介绍了。你可以对照看一下我们的 vimrc 配置文件里的用法，应该就明白了。

Vim 的内置函数真的很多，我也没法一一介绍。你可以稍作浏览，了解其大概，然后在使用中根据需要查询。别忘了，在看 Vim 脚本时，在关键字上按下 K 就可以查看这个关键字的帮助，如下图所示：

在 Vim 脚本里使用 K 键查看帮助

风格指南

结束 Vim 脚本的介绍之前，我向你推荐一下 Google 出品的 Vim 脚本风格指南，Google Vimscript Style Guide。写一种语言，有一个风格指南肯定是会有帮助的，尤其对于初学者而言。

Python 集成（选学）

Vim 脚本功能再强大，也还是一种小众的编程语言。所以，Vim 里内置了跟多种脚本语言的集成，包括：

Python

Perl

Tcl

Ruby

Lua

MzScheme

由于 Python 的高流行度，目前 Vim 插件里常常见到对 Python 的要求——至少我还没有用过哪个插件要求有其他语言的支持。所以，在这儿我就以 Python 为例，简单介绍一下 Vim 对其他脚本语言的支持。各个语言当然有不同的特性，但支持的方式非常相似，可以说是大同小异。

这部分作为选学提供，相当于本讲内部的一个小加餐。Python 程序员一定要把这部分读完，其他同学则可以选择跳到内容小结。

Vim 很早就支持了 Python 2，Vim 的命令 python（缩写 py）就是用来执行 Python 2 的代码的。后来，Vim 也支持了 Python 3，使用 python3（缩写 py3）来执行 Python 3 的代码。鉴于 Python 的代码还是有不少是 2、3 兼容的，Vim 还有命令 pythonx（缩写 pyx）可以自动选择一个可用的 Python 版本来执行。

我在拓展 3 里给出了一段代码，用 Python 来检测当前目录是不是在一个 Git 库里。我们先用 pythonx 命令定义了一个 Python 函数，然后用 pyxeval 函数来调用该函数。这就是一种典型的使用方式：在 Python 里定义某个功能，然后在 Vim 脚本里调用该功能。这种情况下，Python 部分的代码一般不需要对 Vim 有任何特殊处理，只是简单实现某个特定功能。

下面是另一个小例子，通过 Python 来获得当前时区和协调世界时的时间差值（对于中国，应当返回 ␣+0800）：

function! Timezone()
  if has('pythonx')
pythonx << EOF
import time

def my_timezone():
    is_dst = time.daylight and time.localtime().tm_isdst
    offset = time.altzone if is_dst else time.timezone
    (hours, seconds) = divmod(abs(offset), 3600)
    if offset > 0: hours = -hours
    minutes = seconds // 60
    return '{:+03d}{:02d}'.format(hours, minutes)
EOF
    return ' ' . pyxeval('my_timezone()')
  else
    return ''
  endif
endfunction

从 pythonx << EOF 到 EOF，中间是 Python 代码，定义了一个叫 my_timezone 的函数，我们然后调用该函数来获得结果。对于不支持 Python 的情况，我们就直接返回一个空字符串了。

另一种更复杂的情况是，我们的主干处理逻辑就放在 Python 里。这种情况下，我们就需要在 Python 里调用 Vim 的功能了。在 Vim 调用 Python 代码时，Python 可以访问 vim 模块，其中提供多个 Vim 的专门方法和对象，如：

vim.command 可以执行 Vim 的命令

vim.eval 可以对表达式进行估值

vim.buffers 代表 Vim 里的缓冲区

vim.windows 代表当前标签页里的 Vim 窗口

vim.tabpages 代表 Vim 里的标签页

vim.current 代表各种 Vim 的“当前”对象（详见 :help python-current），包括行、缓冲区、窗口等

此外，在拓展 2 里我们给出的使用 pyxf 来执行一个 Python 脚本文件，也是一种在 Vim 里调用 Python 的方式（详见 :help pyxfile）。那段 clang-format 的代码，总体上也就是访问 vim.current.buffer 对象，调用外部命令格式化指定行，然后把修改的内容写回到 Vim 缓冲区里。

内容小结

好了，我们的 Vim 脚本介绍就到这里了。这一讲和大部分其他讲不同，只是给了你一个 Vim 脚本的概览，目的是让你全面了解一下 Vim 脚本，能够读懂一般的 Vim 脚本，而不是真正教会你如何去写脚本。这讲的主要知识点是：

Vim 脚本的基本语法，包括变量、数字、字符串、复杂数据结构、表达式、控制结构和函数

Vim 的专门特性，包括变量的前缀、脚本相关命令、Vim 里的事件和内置函数

Vim 脚本风格指南

Vim 对 Python 等其他脚本语言的支持

作为一门编程语言，只有在实践中不断操练，才能真正学会它的使用。如果你对 Vim 脚本有兴趣的话，我们下一讲会剖析几个 Vim 脚本来分析一下，让你有更深入的体会。

课后练习

请查看几个现有的 Vim 脚本来仔细分析一下，理解各行的意义。建议可以从我们在 vimrc 配置文件中包含的 vimrc_example.vim 开始，然后查看其中使用的 defaults.vim。别忘了，我们可以使用普通模式快捷键 gf 或 <C-W>f 直接跳转到光标下的文件里。

如果遇到什么问题，欢迎留言和我讨论。我们下一讲再见！

15｜插件荟萃：不可或缺的插件

你好，我是吴咏炜。

关于 Vim 的基本知识，我们已经讨论得差不多了。下面，我们需要的是练习和积累，在实践中成长。在今天的这一讲里，我们就来看看一些我们之前还没来得及介绍、但真的“必需”的插件，让你的开发效率再进行一次大幅提升。

跟之前各讲比起来，这讲会比较轻松。不过，在你已经学过了 Vim 的基本原理之后（特别是如果你学习了拓展 4 的样例，进一步了解了插件代码是如何编写的话），我希望你看到这些插件时，不仅可以看到它们的外观和用法，而且还能大概知道它们的工作原理。这样，你就不再仅仅是一个初级用户，而是已经晋升为真正的 Vim 高手，能够根据自己的需要进行定制，甚至是“魔改”了。

Fugitive

对于大部分开发者来说，使用 Git 应该已经和呼吸空气一样自然了吧。我跟很多工作经历丰富的开发者一样，从 CVS 的年代开始（那时我还一直维护着 CVSMenu），经历了 SVN，然后看着 Git 慢慢一统天下，号令江湖。在 Vim 中如何高效地使用 Git，当然也就成了个不得不讨论的话题。

今天，我会介绍两个 Git 插件。一个是重量级的、功能很多的 Fugitive，一个是集中在几个特色小功能上的 GitGutter。下面我们就从 Fugitive 开始。

安装和配置

Fugitive 是纯 Vim 脚本，我们使用包管理器安装 tpope/vim-fugitive 即可（对了，他也是 Time Pope 大牛写的）。

这个插件基本上不需要配置，即装即用。

使用

Fugitive 是一个全功能的 Git 支持插件，支持在 Vim 里直接使用 Git 命令：你只需要把“G”变成大写即可。比如，原本你要在 shell 使用 git log，现在在 Vim 里使用 :Git log 命令即可。结果会在一个 Vim 窗口里打开，并且有着语法加亮的效果：

:Git log 命令的效果

Fugitive 有很多命令，你可以在它的帮助文件里找到较为完整的信息。下面我从使用的角度说明一下最常用的命令，重点是对当前文件操作的命令。像 :Git pull 这样的命令，我就略过不讲啦。

首先，要把当前文件的修改存盘并加到 Git 的暂存区，可以使用 :Gwrite。

反过来，要放弃当前的修改，回到暂存区的状态，可以使用 :Gread。

如果你需要对当前文件进行移动、更名或删除，可以分别使用 :GMove、:GRename、:GDelete。跟命令行上的 git 命令不同，你不需要给出当前文件的名字。

要在 quickfix 窗口里查看当前文件的修改历史，可以使用 :0Gclog（Gclog 是查看整个项目的历史，对大项目使用 quickfix 窗口可能会有性能问题）。同时，你可以通过历史窗口，回到这个文件的任一版本。

:0Gclog 命令的效果

要查看文件的某一行是谁最后修改的，可以使用 :Git blame 命令。你不需要给出当前文件的名字，而且 Fugitive 会把输出放到一个跟当前文件编辑窗口联动的新窗口里，并进行了色彩加亮。

:Git blame 命令的效果

要比较当前文件和暂存区版本的区别，可以使用命令 :Gvdiff。

:Gvdiff 命令的效果

要查看当前 Git 库的状态，可以直接输入 :Git 命令。在结果缓冲区里，有很多正常模式命令可用（见 :help fugitive-staging-maps）。其中最实用的可能是：

使用 s 来把文件（整个文件）或光标下的修改（部分修改）加到暂存区中

使用 u 来重置加入暂存区的修改（撤销 s）

使用 = 来切换开关这个文件的内联 diff 显示

使用 o 来在新分割的窗口中打开文件

使用 dv 来比较文件和暂存区版本的区别

使用 cc 来签入（commit）当前暂存区中的文件，相当于 :Git commit 命令

在 :Git 命令后使用部分快捷键

最后，:Git push 本来是不需要解释的，但你可能会高兴知道，在安装了 AsyncRun（见第 8 讲）之后，这个操作自动就是异步的，不会在操作完成之前妨碍你继续进行编辑。

此外，所有的 :Git … 命令都可以打字成 :G …，事实上，之前给出的命令清单里，连这个空格都不要，所以你可以输入 :Gpush 来代替 :G push。不过，作者宣称 :Gblame 和 :Gpush 这样的命令在将来的版本里可能会被删除，所以你如果在用 :Gpush 这样的命令的话，可能需要知道一下这个事情。

你已经可以看到，Fugitive 的命令是相当复杂的，毕竟 Git 也很复杂。我上面讨论的，只是它的功能的一部分。你可以在它的帮助文件中了解进一步的信息（:help fugitive）。

GitGutter

如果说 Fugitive 是一个全功能 Git 插件的话，GitGutter 就只是 Git 使用中的一个非常小的点。它的功能列出来也可以有不少，但跟 Fugitive 不同，它专注在单个文件内的修改管理上。

安装和配置

你需要使用包管理器安装 airblade/vim-gitgutter。如果你使用了一个比较知名的配色方案的话，多半你不需要额外配置了。GitGutter 会试图从配色方案里找出合适的颜色，但这个方法对配色方案会比较挑，尤其是对默认配色方案不适用。如果你觉得边栏或边栏里符号的颜色不让人满意的话，可以在 vimrc 配置文件中作类似于下面的配置：

highlight! link SignColumn LineNr
highlight GitGutterAdd    guifg=#009900 ctermfg=2
highlight GitGutterChange guifg=#bbbb00 ctermfg=3
highlight GitGutterDelete guifg=#ff2222 ctermfg=1
let g:gitgutter_set_sign_backgrounds = 1

没有上面那样的配置的话，在简单配色方案（如 Vim 的默认配色方案）下，GitGutter 的默认设置会出现突兀的边栏和无色的符号，丑得让人无法接受。

默认配色方案下的 GitGutter 效果

使用

下面的动图展示了 GitGutter 的核心功能（使用 gruvbox 配色方案）：

GitGutter 的功能展示

简单说明一下：

在 Vim 的边栏里用 +、- 等符号和合适的配色来标注哪些行有了修改

[c 和 ]c 可以用来跳转到上一个和下一个修改的位置

<Leader>hp 可以将光标下的修改块和缓存区中的内容进行对比

<Leader>hs 可以将光标下的修改块加入到暂存区中

<Leader>hu 可以恢复暂存区中的内容

换句话说，GitGutter 的重点是一个文件内修改的管理——这个功能 Fugitive 可以说也有（在 :Git 命令下管理），但不及 GitGutter 直观。使用 GitGutter，你可以在编辑文件时，立即选择把部分修改加入暂存区，方便后续的签入。因此，虽然看起来 GitGutter 的功能不多，我使用它的频度跟 Fugitive 差不多。

Airline

如果你认为界面的美观很重要的话，你多半也会喜欢 Airline。Airline 应该可以算是 Vim 中最流行的插件之一了。它会和很多其他知名 Vim 插件自动进行集成，并在界面上展示非常丰富的信息。事实上，我在开篇词中给出的 Vim 界面截图中就使用了 Airline。我们这儿再重新展示一下。

启用了 Airline 的 Vim 界面

靠近底部、开头是“NORMAL”的那行状态栏就是由 Airline 控制的。我们可以看到其中有非常丰富的信息：

当前的模式直接显示在状态栏上，并且每种模式都有自己的特别颜色，可以一目了然（在 gruvbox 提供的配色里，插入模式和可视模式的背景色都比目前的要突出得多）。

后面紧跟着的分支符号和“master”是 Airline 检测到了 Fugitive，通过 Fugitive 获得了分支名称。

后面跟着的是文件名和修改标记。

再后面，我们可以看到光标所在的位置的函数名，这是 Airline 通过 Tagbar 获得的。

随后，我们看到文件类型 cpp。

最后，我们看到光标位置在文件中的行数百分比、行号、总行数和列号，通过一些花哨的 Unicode 符号来进行分隔。

这些只是 Airline 功能的一部分。事实上，它内部有对超过 50 个 Vim 插件的集成扩展，会自动检测它们的存在，并显示相关信息。要看到这些扩展的完整列表和当前状态，可以使用 :AirlineExtensions 命令。

安装和配置

Airline 本身的安装非常简单，通过包管理器安装 vim-airline/vim-airline 即可。不过，要显示出比较好（kù）的效果，你需要有一个支持它用到的特殊字符的字体。这些字体常常被叫做 Powerline 字体，原因是一个叫 Powerline 的插件最早开始使用这些特殊符号。（Airline 也算是 Powerline 的一个替代品了，不过，两者还是有点区别的，其中之一就是，Powerline 要求 Python 支持，而 Airline 是纯 Vim 脚本。）

下面这几个是我比较推荐的：

Source Code Pro：一款设计非常优秀的字体，不花哨，但耐看

Fira Code：这是一款非常花哨的好字体（如果你疑惑上一张图里居然有“→”，就是这个字体对 -> 的特殊渲染），也支持 Airline 需要的特殊符号

DejaVu Sans Mono for Powerline：著名的开源字体 DejaVu Sans Mono 加入了特殊符号支持的版本

在我目前的 vimrc 配置文件中，我对 Airline 进行了下面这样的配置：

let g:airline_powerline_fonts = 1
let g:airline#extensions#tabline#enabled = 1
let g:airline#extensions#tabline#buffer_nr_show = 1
let g:airline#extensions#tabline#overflow_marker = '…'
let g:airline#extensions#tabline#show_tab_nr = 0

首先，我告诉 Airline，可以使用 Powerline 字体来显示特殊符号。

其次，我启用 tabline 扩展，在顶部显示缓冲区列表。

再次，我让 tabline 显示缓冲区的编号，这样方便我使用数字加 <C-^> 来切换缓冲区。

然后，我使用 … 来表示省略（单个字符，而非占据三列的三个点），这样可以节约一点屏幕空间。

最后，我一般不在一个 Vim 会话中打开很多个标签页，一般使用上一个、下一个标签页的命令来进行切换，所以也就不需要显示标签页的编号了，免得和缓冲区编号两个显示在一起会比较乱。

这样配置下的效果，可以参见下面的截图（图中的字体是 Source Code Pro）：

配置了显示缓冲区编号的 Vim 界面

哦，对了，下面的那个闪电符号也是 Airline 搞出来了，提醒你当前的 Git 库里有没有提交的更改。

使用

Airline 的使用基本上是自动的，安装、配置完了，差不多就不用去管它了。留意看它的提示就行了。比如，它会在右下角进行可能错误的显示提示，包括行尾的空格（trailing space），YCM 发现的代码错误，等等。

Airline 还提供了一些命令，如 :AirlineExtensions 和 AirlineToggle。这些你直接看帮助文档（:help airline）就应该可以清楚了。

NERDCommenter

我们前面已经介绍过了 NERDTree，而同一个作者写的另外一个插件 NERDCommenter，也在我的不可或缺的 Vim 插件的列表上。它提供的是又一个开发常用的功能，对某个代码块或代码行加上注释，及反过来把注释去掉。

安装和配置

我们需要在包管理器中安装 preservim/nerdcommenter。然后，我一般在 vimrc 配置文件中加入下面的代码，让 NERDCommenter 不要在终端 Vim 中加入菜单，干扰我使用 <F10> 查看最近的文件：

if !has('gui_running')
  let g:NERDMenuMode = 0
endif

使用

NERDTree 提供的命令，有些可以工作于当前行或选定的行内部分字符，有些则只能工作于整行，即使用可视模式的话选行内部分字符相当于选了整行。能够适用于行内的有下面这些命令（也能适用于整行）：

<Leader>cc 把代码变为注释

<Leader>cu 把注释起止符剥掉，恢复原先的代码

下面这些则只能工作于完整的一行或多行上：

<Leader>c<Space> 用来切换注释和非注释

<Leader>cb 用来加上“美观”的注释

<Leader>cs 用来加上“性感”的注释

取决于具体的语言，行内注释、“美观”注释和“性感”注释不一定全部都适用（比如，在 Python 里就没有在一行内部开始加结束的注释）。我们拿支持所有这些情况的 C++ 代码来演示一下这些不同的注释类型：

NERDCommenter 的多种不同注释类型

目前 NERDCommenter 支持的文件类型也有一百种左右了，应该说还是非常全面的。对于调试代码，它也是一个非常重要的工具。

Visual-Multi

我们今天要介绍的最后一个重要插件是 Visual-Multi，它允许你把“光标”同时放在多个不同的位置上，然后下面的命令对所有的位置都有效。利用这个插件，我们要做变量重命名之类的操作就会变得非常简单。

安装和配置

我们利用包管理器安装 mg979/vim-visual-multi 即可。这个插件的默认配置挺合理，我目前没有发现要修改的内容。

使用

这个插件的核心功能，是帮你选中多个不同的位置，然后我们就可以使用正常的编辑命令，包括 c、d、i 等，来进行编辑。

所以，下一个问题自然是，我们该如何选中多个不同的位置呢？

Visual-Multi 里还是提供了好几种不同的选择方式的（:help vm-quickref），我们就只讨论最常见的情况，根据搜索条件来进行选择。

在 Visual-Multi 里，特别的搜索命令是 \\/。跟普通的搜索命令一样，在输入的过程中我们即可看到屏幕上的匹配。但在搜索完成之后，n 命令不再仅仅意味着跳转到下一个匹配位置，而是选中当前的位置，并跳转到下一个匹配位置。如果想要跳过一个位置，则可以使用 q 命令。想要直接选中所有匹配的位置的话，可以使用 \\A 命令。

跟我们可以用 * 命令直接搜索光标下的单词一样，在 Visual-Multi 里我们也有一个快捷搜索命令 <C-N>。之后，我们的使用方式就一样了。下面，我就演示一下怎么用这个快捷搜索命令加其他的 Visual-Multi 命令来完成第 10 讲综合实验里的那个批量代码更名操作。

使用 Visual-Multi 来完成类型更名操作

看明白了吗？保险起见，我稍微解释一下吧。

我首先使用 <C-N> 命令搜索 \<smart_ptr\>，开始了特殊的 Visual-Multi 模式，扩展模式（相当于可视模式）。

然后我使用了 \\A 命令选中所有的匹配位置。

由于文件名不应该更改，我使用 q 键跳过第一个匹配。

随后，我移动光标位置，缩小选中的位置。

之后，我就可以使用 c 命令直接输入需修改部分的新名字了。

最后，我按一次 <Esc> 退出插入模式，再按一次 <Esc> 退出 Visual-Multi 模式（从屏幕底部也可以看到文字提示）。

Visual-Multi 也支持用 \\\ 命令来手工增加一个光标位置，也有其他的一些命令和功能。这些，就请你自己慢慢摸索练习了。毕竟，它的帮助文件的篇幅就比我这整个一讲还多呢。

内容小结

今天我们介绍了五个非常重要的插件。其中，Fugitive 和 GitGutter 是用来支持 Git 版本管理的，Airline 是提供界面上的丰富信息提示的，NERDCommenter 和 Visual-Multi 则可以让一些代码的编辑任务变得更加高效。

本讲我们的配置文件中加入了这些不可或缺的插件，对应的标签是 l15-unix 和 l15-windows。

课后练习

高效地用好这些插件，可以让你的 Vim 使用效率再上一层楼。我们今天的课后练习，就主要是温习一下学到的知识了：

安装好这些插件（应该每个都很有用）

打开 Git 管理下的我们的配置文件，体验一下 Airline 的显示效果

随便做点修改，并存盘，观察 GitGutter 和 Airline 对边栏和状态栏的提示变化

使用 :Git 命令打开 Fugitive 面板，在修改的文件上用 = 观察其中的修改

使用 s 把修改加到 Git 的暂存区，然后用 cc 签入修改

关闭 Fugitive 面板，回到 .vimrc 或 _vimrc 文件，用 :0Gclog 浏览文件的历史，并查看这个文件的历史状态

用 1<C-^> 命令或 :e .vimrc 之类的命令回到原始文件，然后用 :Git blame 命令查看文件每行的修改历史

如果光标在左侧窗口，使用 gq 命令，如果光标在右侧，使用 <C-W>o 命令，回到只有配置文件的状态

使用 NERDCommenter 的命令，如 <Leader>cb，来修改代码行的注释状态

使用 Visual-Multi，把代码中的 source 全部改成 so

最后，如果你不知道怎么回退刚才对 Git 库的修改的话，可以手工输入下面的 Git 命令：

git reset HEAD~1
git reset --hard

在基本用法掌握之后，有学习余力的同学可以再仔细阅读一下帮助文件，了解我没有描述的用法。

如果有任何问题或疑问，欢迎留言和我讨论！

16｜终端和 GDB 支持：不离开 Vim 完成开发任务

你好，我是吴咏炜。

早在 Vim 和 Emacs 的“圣战”时期，Emacs 有个功能可是 Vim 用户一直暗暗垂涎的，那就是可以集成 GDB 来调试程序。Emacs 之所以能够实现这个功能，是因为它可以模拟一个终端环境，像终端一样跟一个程序进行输入输出的交互。这样一来，我们不离开编辑器，也能调试程序，既可以方便地看到目前执行在源代码的第几行，也可以直接在编辑器里跟执行中的程序进行交互。

很多主流的开发环境都支持类似的功能。但 Vim 一直不支持这样的功能，直到 Vim 8。虽然到得有点晚，但 Vim 也算是厚积薄发，利用 libvterm 给出了完整的终端支持。今天，我们就拿终端窗口支持和 GDB 支持，作为我们最后的技术话题来介绍了。

终端窗口支持

基本用法

使用 :terminal（缩写 :term）命令，我们可以在 Vim 的窗口中运行终端模拟器。基本的用法就是下面两种：

使用 :terminal，后面不跟其他命令，分割一个新窗口，并使用默认的 shell 程序进行终端模拟；shell 退出后窗口自动关闭（可用使用命令参数 ++noclose 改变这一行为）。

使用 :terminal 命令 的方式，分割一个新窗口，在其中运行指定的命令并进行终端模拟；命令执行完成退出后窗口不自动关闭，保留执行中显示的信息（可用使用命令参数 ++close 改变这一行为）。

跟其他的多窗口命令一样，:terminal 默认会进行横向分割，但你也可以在 terminal 前面加上 vert 来进行纵向分割，或加上 tab 来把终端窗口打开到一个新的标签页里。

跟 quickfix 窗口里只能看到程序的输出不同，在终端模拟器里我们既可以看到程序的输出，也可以向程序提供输入。同时，这个终端模拟器像一个真正的终端一样，能够支持色彩和其他的文本控制。你甚至可以在里面运行 Vim，就像 Matrix 电影里层层嵌套的世界一样。

开了两个终端窗口的 Vim，其中上面那个又再次运行 Vim

当然，从实用的角度，我并不建议你这么做——那样可能会让人头昏，并且容易在使用 <C-W> 和 :q 这样的命令时，出现结果跟自己预想不一致的情况。

终端模拟器的行为应当跟普通的终端一致；因此在 Vim 的终端模拟器里，你可以直接使用的命令跟一般的 Vim 窗口很不一样。毕竟，你在终端模拟器里输入 : 时，肯定不是想进入 Vim 的命令行模式吧？这时候，你需要知道下面这些在“终端作业模式”下的特殊命令（完整列表见 :help t_CTRL-W）：

<C-W>N（注意大写）或 <C-\><C-N> 退出终端作业模式，进入终端普通模式。这时终端窗口变成一个普通的文本窗口（终端缓冲区），不再显示色彩，但可以像普通的只读窗口一样自由使用，只是不能修改其中的内容而已。按下 a 或 i 可重新激活终端模拟器，进入终端作业模式。

<C-W>" 后面跟寄存器号，表示粘贴该寄存器中的内容到终端里。

<C-W>: 相当于普通窗口中的 :，执行命令行模式的命令。

<C-W>. 可以给终端窗口发送一个普通的 Ctrl-W。

<C-W><C-\> 可以给终端窗口发送一个普通的 Ctrl-\。

大部分的 <C-W> 开始的命令仍然可以使用，如窗口跳转命令（后面跟 j、k 等）、窗口大小调整命令（后面跟 +、_ 等），等等。

需要注意，终端模拟器里的光标只能用正常终端里的光标移动键来移动，比如在 Bash 默认配置下，可以用 <C-A> 或 <Home> 移到行首，用 <C-E> 或 <End> 移到行尾等。在退出终端作业模式后，光标就只是普通文本窗口的光标，不会影响终端模式里的光标位置——在你按下 a 或 i 时，光标还是在原来的位置，而不是退出终端作业模式后你移动到的新位置。你也不能修改终端缓冲区中的内容。只要稍微仔细想一想，你就知道这些是完全符合逻辑的。

当你从终端窗口切到另外一个窗口时，终端窗口里面的程序仍然在继续运行；如果你不退出终端作业模式的话，终端窗口里面的内容也会持续更新，跟正常的终端行为一致。要结束终端运行的话（而不只是临时退出终端模式），也跟普通的终端情况一下，可使用 exit 命令或 <C-D>。如果由于某种原因无法正常退出终端的话，则可以使用 <C-W><C-C> 来强行退出。

使用提示

如果你觉得自己不会在终端里另外启动 Vim，似乎也就很少有机会用到 <Esc> 了，那我们干吗不把这个键用作退出终端作业模式呢？说干就干：

tnoremap <Esc>      <C-\><C-N>
tnoremap <C-V><Esc> <Esc>

前缀 t 表示在终端作业模式下的键映射。我们把 <Esc> 映射到我们上面说的退出终端作业模式的快捷键；同时，我们又把 <C-V><Esc> 这一在终端里等价于 <Esc> 的按键组合映射为 <Esc>，这样万一我们需要 <Esc>，仍然可以用一种较为自然的方式获得这个按键。

遗憾的是，在 Unix 终端的情况下，很多功能键本身包含 <Esc>，因而会误触发这个键映射。对于这种情况，我们使用下面的键映射，用连按两下 <Esc> 退出终端作业模式效果更好：

tnoremap <Esc><Esc> <C-\><C-N>

此外，对于大部分人而言（像 Bram 这样，用 Vim 调试 Vim，不属于大众需求吧），在 Vim 的终端模式里启动 Vim，恐怕是失误的可能性最大。为了防止这样的失误发生，我们可以在 Vim 启动时检查一下，检测这种嵌套的 Vim 使用。你只需要把下面的代码加到 vimrc 配置文件的开头即可：

if exists('$VIM_TERMINAL')
  echoerr 'Do not run Vim inside a Vim terminal'
  quit
endif

你可以试验一下在 Vim 的终端窗口里再运行 Vim，看一下上面的代码产生的出错效果。

终端的用途

说了这么多，你可能有点疑惑，单独起一个终端有什么问题吗？我为什么要在 Vim 里运行终端呢？

我是这么理解的：

方便。特别在远程连接的时候，有可能新开一个连接在某些环境里需要特别的认证，比较麻烦。即使连接没有任何障碍，你总还需要重新 cd 到工作目录里吧？而如果在一个现有的 Vim 会话里开一个新的终端，可以一个命令搞定，然后用你已经很熟悉的 Vim 命令在不同的窗口或标签页里切换。

文本。我们可以从终端作业模式切换到终端普通模式，然后用我们熟悉的 Vim 命令来对缓冲区中的文本进行搜索、复制等处理工作。

控制。你可以发送命令给终端，也可以读取终端屏幕上的信息。这样，事实上就打开了一片新天地，可以在 Vim 里做很多之前做不到的事情，比如，用 Vim 来比较两个屏幕输出的区别（:help terminal-diff）。

终端窗口相关的函数名称都以 term_ 打头（可以查看帮助文件 :help terminal-function-details）。比如，如果我们想要用程序向缓冲区编号为 2（可以用 :ls 和 :echo term_list() 等命令来检查）的终端发送 ls 命令来显示当前目录下的文件列表的话，我们可以使用（注意转义字符序列要求使用双引号）：

call term_sendkeys(2, "ls\n")

下面这个比较无聊的例子，可以用来获取 ~/.vim 目录下的文件清单：

let term_nbr = term_start('bash')
call term_wait(term_nbr, 100)
let line_pos1 = term_getcursor(term_nbr)[0]
call term_sendkeys(term_nbr, "ls ~/.vim|cat\n")
call term_wait(term_nbr, 500)
let line_pos2 = term_getcursor(term_nbr)[0]
let result = []
let line_pos1 += 1
while line_pos1 < line_pos2
  call add(result, term_getline(term_nbr, line_pos1))
  let line_pos1 += 1
endwhile
call term_sendkeys(term_nbr, "\<C-D>")
while term_getstatus(term_nbr) != 'finished'
  call term_wait(term_nbr, 100)
endwhile
exe term_nbr . 'bd'
echo join(result, "\n")

这当然不是完成这件任务的最好方法，但上面的代码展示了终端相关函数的一些基本用法：

我们用 term_start 命令创建一个新的终端，得到终端缓冲区的编号

我们用 term_wait 等待 100 毫秒，待其就绪

我们用 term_getcursor 获取光标的当前行号

我们用 term_sendkeys 发送一个命令到终端上；ls 之后用 cat 是为了防止 ls 看到输出是终端而产生多列的输出

然后我们等待命令执行完成并更新终端

我们获取光标的当前位置，然后用 term_getline 获得上一次的行号和这一次的行号之间的行的内容，放到变量 result 里

我们然后发送一个 <C-D> 到终端，结束作业

然后我们等待到 term_getstaus 返回的状态成为 'finished'，即终端作业已经执行结束

最后我们用缓冲区编号加 bd 命令删除缓冲区（所以屏幕上我们看不到这个终端窗口），并用换行符作为分隔符打印 ls 返回的内容

你可以实际测试一下这个脚本，体会一下这些基本功能。比如，可以把脚本存盘为 test.vim，然后用 :so % 来运行。

GDB 支持

为什么 Vim 直到最近才支持 GDB 呢？因为这真不是件容易的事情啊。为了能在 Vim 里顺畅地使用 GDB，Bram 需要在 Vim 里实现下面这些不同的功能：

终端支持

作业（job）和通道（channel）

窗口工具条、弹出窗口和弹出式菜单

有了这些功能之后，Vim 通过一个内置的插件，就可以提供 GDB 的调试支持了。我们可以通过 :packadd termdebug 命令来加载这个插件，然后通过 :Termdebug 可执行程序名称 来调试一个可执行程序。

下面这个动图可以说明最主要的流程：

在 Vim 里进行调试的过程示例

我简要说明一下需要注意的几点：

:Termdebug 命令会把屏幕分成三个区域，从上到下分别是 gdb 命令行，程序输出，以及含调试控制按钮的源代码窗口。

在最上面的 gdb 窗口中，我们可以输入 gdb 的命令，但程序的输出和纯终端使用 gdb 的情况不同，是在中间的窗口输出的。

最下面的的源代码窗口里，我们有五个按钮可以用，允许习惯图形界面的用户使用鼠标进行操作。我们也可以使用鼠标右键直接在源代码行上设置断点。（当然，我们仍然可以在最上面的 gdb 窗口用命令来完成这些任务。）

鼠标在变量上悬停时，可以显示变量的值。只要 gdb 能打印的信息，它就能用浮动提示显示出来。这比手工使用 gdb 的 p 命令还是要方便多了。

还有一个需要稍微注意的地方是，如果你在不同的作用域有两个同名变量，那浮动提示只能显示当前作用域的变量的信息，即使你把光标放到不在当前作用域的变量上也是如此。这点上，Vim 还是比较笨的——毕竟它不理解代码。

内容小结

这讲我们介绍了 Vim 8 带来的新功能：终端支持。这个功能给 Vim 打开了一片新的天空。使用终端支持，我们可以不离开 Vim 打开一个或多个新的终端窗口，里面可以模拟真正的终端功能，包括色彩控制。我们可以使用 Vim 命令来处理新的终端缓冲区中的文本。我们还可以利用代码来控制这个终端和读取其中的内容。有了这些支持，Vim 也就顺理成章地支持使用 GDB 像集成开发环境一样地调试程序了。

根据我个人的经验，在使用了这个功能之后，我开启新远程连接比之前少了，而经常在一个服务器上只开一个连接，里面开一个 Vim 来完成所需的任务。编译和执行，可以全部在这个 Vim 会话里完成。

本讲我们的配置文件中加入了针对终端窗口的键映射和防 Vim 重入，对应的标签是 l16-unix 和 l16-windows。

课后练习

请尝试使用 :terminal 命令，打开一个新窗口，并在其中进行操作，然后退出终端作业模式，把终端缓冲区中的内容复制到新的缓冲区中。

如果你使用一种可以用 GDB 调试的编译语言的话，也请你尝试一下使用 :Termdebug 命令进行调试。如果你之前用的是纯命令行的 gdb 的话，这个功能还是有很大的易用性提升的。

最后，同样地，如果有任何问题或疑问，欢迎留言和我讨论！

我是吴咏炜，让我们在告别这个课程之前，再道一次再见。

拓展1｜纯文本编辑：使用 Vim 书写中英文文档

你好，我是吴咏炜。

今天是拓展篇的第 1 讲，我想带你对 Vim 的纯文本编辑技巧做一个专项突破。由于 Vim 是在欧美世界诞生的工具，贡献者中也是说英语的人居多，因而它对英文的支持要远远超出其他语言。所以今天，我们就深入讨论一下，如何使用 Vim 来进行纯文本编辑，特别是英文的文本编辑。

熟练掌握这一讲的内容，可以让你使用 Vim 书写中英文文档时都感到游刃有余。如果你有这个需求，一定要亲自动手尝试我提到的这些功能，加深自己的记忆。如果你觉得还需要多花一点时间，消化吸收前几讲的基础知识，也可以先阅读全文，把握要点，之后再回过头来深入学习。

为什么不使用字处理器？

你可能已经开始怀疑了，我为什么要使用 Vim 来进行文字编辑？用 Word 不香么？如果嫌 Word 贵，还有免费的 WPS 啊……

嗯，首先，Word 和 WPS 这些字处理器不是用来生成纯文本文件的。在处理纯文本文件上，它们反而会有诸多劣势，如：

只能本地使用，既不能在远程 Linux 服务器上运行，也不能用 SSH/SCP 的方式打开远程的文件（除非在服务器上启用 Samba 服务，但体验真的不好）

分段和分行一般没有很好的区分

如果存成纯文本的话，格式会全部丢失

最后一句话似乎是废话？还真不是，纯文本文件里面是可以存储格式的，但 Word 和其他字处理软件对于文本类型一般只能支持纯文本或富文本（Rich Text），而富文本虽然包含了格式信息，但却对直接阅读不友好。我想，没有人会去手写富文本文件吧。仍有一些带格式的文本文件比较适合手写，下面这些是其中较为流行的格式：

Tex 和 LaTeX，著名的特别适合写公式的文档系统，在数学和物理学界尤其流行。

DocBook，基于 SGML/XML 的文档系统，可以生成多种不同的输出格式；大量开源软件的文档是用 DocBook 写的。

AsciiDoc，功能和 DocBook 等价、但使用非 XML 的简化语法的文档系统；有的国外技术书出版社接受作者用这种格式提交的稿件。

HTML，HTML 的阅读友好性一般，但胜在熟悉的人多。

Markdown，Markdown 的阅读体验非常友好，因而它虽然最“年轻”却最流行。接下来，我们就介绍一下这种文件类型。

Markdown 简介

Markdown 是由 John Gruber（约翰 · 格鲁伯）在 2004 年发明的，它不是一种标准化的格式，存在着多个实现，功能也并不完备。尽管如此，由于它轻量、易写、易读，很快就在互联网上流行开了。在 GitHub 上，现在 README 文件一般都使用 Markdown。

GitHub 上的显示效果和 Markdown 源码

通过工具（很多是开源的，如 pandoc），Markdown 可以很容易地转成网页、PDF 等其他格式，同时也很适合以纯文本的形式阅读。而 HTML、DocBook 等格式实际上是不太适合人直接看源代码来阅读的。此外，极客时间，以及很多写作平台，用的也是 Markdown。

事实上，在处理代码相关的文档时，Word 还真没有 Markdown 方便。Word 的某些“自动”功能，如把直引号替换为弯引号，会对键入代码造成干扰。而对嵌入的代码进行语法加亮，则是 Word 里没有、而 Markdown 里非常成熟的功能了……

不过，我们今天的重点不是 Markdown，而是怎么使用 Vim 来编辑上面提到的所有文件格式。没有了图形界面的简单化处理，你可以把控文本的一切细节，但同时，Vim 的语法加亮和文字编辑功能本身，也会让你编辑起来非常得心应手。鉴于 Markdown 的优势，我们今天的例子还是会用 Markdown。这样，你学了这一讲之后，至少会知道怎么高效地给开源项目写个 README 文件。

英文文本编辑

考虑到 Markdown 等标准在中文处理的标准化上面有先天不足，我们先学习文档的主语言为英文的情况。我们可以先看一眼下面的截图：

一个 README 文档的开头部分

然后对比一下它的 Markdown 源代码在 Vim 中的展示效果：

该 README 文档开头部分的 Markdown 源码

我们明显可以看到，用 Vim 编辑 Markdown 文件时，虽然没有浏览器里显示得那么美观，但在使用等宽字体的前提下仍有着合适的语法加亮。

有两个细节值得关注一下：

跨行的那个链接加亮正常（我在至少两种其他环境下看到在方括号跨行时链接就无法得到正确的处理）。

单词“LICENCE”在 Vim 展示时也使用了斜体（一对星号中间的内容在 Markdown 里就是使用斜体强调），并且如果光标移出该行，星号会被自动隐藏，更方便阅读。

需要注意，在网页中的换行位置和源代码中的换行位置是不一样的。源代码中存在真正的换行（上一讲提到的 LF 或 CR LF 构成的行尾结束符）；而转换到网页显示之后，单个换行只相当于空格字符，浏览器里一行应当显示多少字符仍然由浏览器的宽度和样式表来决定。这就是标准的 Markdown 的行为了。

行宽设置

英文文本文件的惯例仍然是一行放不超过 80 个字符，所以在源代码中仍然是有手工断行的。这个习惯是为阅读“源代码”优化的：可以看到，上面这个 Markdown 文件虽然在浏览器里查看效果更好，以纯文本的形式查看也是非常干干净净、毫无问题——只要你的编辑器列宽大于等于 80 就行了。

我们上一讲已经提到了文本宽度选项 textwidth。在对英文文本编辑时，这个选项的推荐数值通常是 72，比标准列宽 80 稍窄。这个设置有历史原因，但更重要的是，这也是经过历史验证对人阅读比较舒适的设定：既不会产生频繁的换行而打乱阅读节奏，也不会因为行太长而发生寻找下一行起始位置的困难。

被誉为“排版圣经”的 The Elements of Typographic Style 对行宽有这样的描述：

Anything between 45 to 75 characters is widely regarded as a satisfactory length of line for a single-column page. . . . The 66-character line . . . is widely regarded as ideal.

我们说列宽 72，是指最大值，而使用 72 产生的实际文本宽度，差不多就是落在 66 这个理想值附近了。一些编码规范，如 Python 的 PEP 8，也约定对文档内容的列宽数值应当是 72。大部分编码规范对代码宽度的约定稍宽松些，一般是 79 或 80。

格式化选项

我们上一讲已经提到格式化选项 formatoptions（缩写 fo），今天我们来稍微展开一下，看看这些格式化选项对我们写文档有什么样的影响。

在 Vim 里，fo 选项的默认值是 tcq。根据 Vim 的帮助文档，它们的含义是：

t：使用 textwidth 自动回绕文本。

c：使用 textwidth 自动回绕注释，自动插入当前注释前导符。

q：允许 gq 排版时排版注释。

不过，根据你编辑的内容的语法，这个选项内容可能会发生变化。如运行支持文件里的 ftplugin/c.vim 里有下面的语句：

" Set 'formatoptions' to break comment lines but not other lines,
" and insert the comment leader when hitting <CR> or using "o".
setlocal fo-=t fo+=croql

即，正常输入非注释内容时，不进行回绕。但对注释还是要使用回绕的。另外几个我们还没检查过的选项是：

r：在插入模式按回车时，自动插入当前注释前导符。

o：在普通模式按 o 或者 O 时，自动插入当前注释前导符。

l：插入模式不分行: 当一行已经超过 textwidth 时，插入命令不会自动排版。

上一讲我们用到的 n 则是：

n：在对文本排版时，识别编号的列表。实际上，这里使用了 formatlistpat 选项，所以可以使用任何类型的列表。出现在数字之后的文本缩进距离被应用到后面的行。数字之后可以有可选的 .、:、)、] 或者 }。注意 autoindent 也必须同时置位。

formatlistpat 的缺省值是 ^\s*\d\+[\]:.)}\t ]\s*。也就是说，起始处有可选的空格，然后是至少一个数字，之后必须跟 .、:、)、]、}、制表符或空格中的一个，随后是可选的若干空格。

运行支持文件里的 ftplugin/markdown.vim 会对 formatlistpat 做额外的设定，使得 Vim 不仅可以识别数字列表，也能识别用 - 等字符开始的无序列表。有兴趣的同学可以自己分析一下。

如果你希望有比较接近字处理器的体验，不用自己手工断行，下面两个选项对英文文本编辑比较重要：

w：拖尾的空格指示下一行继续同一个段落。而以非空白字符结束的行结束一个段落。

a：自动排版段落。每当文本被插入或者删除时，段落都会自动进行排版。

这两个选项结合的效果，会让在 Vim 里编辑的效果在某种程度上接近字处理器：你会看到在某行增删内容会自动导致下面的行跟着卷动，你可以通过下面的动图看下效果（我使用了 Vim 的 listchars 选项来加亮行尾空格和行尾结束符）：

对使用了 a 和 w 两个 formatoptions 的文本进行编辑

段中不换行的文本

到现在为止，我们讨论的英文文本编辑，基本都是行尾结束符不代表真正分行的情况。这种方式最常见，但也存在例外，如有些网站不使用标准的 Markdown 规则，把行尾结束符直接就当成换行了。在这样的情况下，我们就不应该在一段中间手工插入换行符。不过，由于这种方式不是 Vim 的“自然”处理方式，我们需要修改一些选项和处理习惯来应对这种情况。

首先，Vim 在默认配置下会在窗口宽度不足时自动折行显示，但不会对折行的位置进行特殊挑选，很可能会在单词的中间折行。我们可以设置选项 linebreak，告诉 Vim 要在 breakat 的字符上才可以折行。默认的 breakat 设置包含了空白字符和英文标点符号，因此直接可以在英文环境中进行使用。下图展示了 linebreak 的效果：

设置 linebreak 的效果

在这种模式下进行编辑时，另外一个要注意的问题是 j 和 k 移动的是一个物理行，而非屏幕行。这和很多编辑器的行为是不同的。要让光标一次移动一个屏幕行，需要的按键是 gj 和 gk。如果你希望 <Up> 和 <Down> 的行为跟主流的编辑器一致，可以考虑以下的设定：

" 修改光标上下键一次移动一个屏幕行
nnoremap <Up>        gk
inoremap <Up>   <C-O>gk
nnoremap <Down>      gj
inoremap <Down> <C-O>gj

最后，如果之前换行符不代表分段，现在你希望换行即分段，你可以用 J（代表 join，连接）命令把多行重新连接成一行。这个命令在某种程度上可以看作是 gq 的逆命令。由于这个命令根据字符的类型来决定是否插入空格和插入几个空格（参考 :help 'joinspaces'），它并不能简单地用替换命令来代替。如果考虑最简单的情况（:set nojoinspaces），那至少在 formatoptions 中含有 w 时，我们可以用下面的替换命令来连接同一段的所有行:

:%s/\([^\n]\)\s\+\n\s*\([^\n]\)/\1 \2/

事实上，使用 joinspaces 和 Markdown 的双行尾空格代表换行是潜在会冲突的。在 Markdown 中使用 w 时，应当使用 nojoinspaces。

模式行

到现在为止，我们讨论的好些选项，不仅不适合作为全局选项，也不适合作为某一文件类型的选项，而更适合用作单个文件的选项。Vim 也确实提供了这样的功能，叫做模式行（:help modeline），能自己使用 :setlocal 仅对当前缓冲区设置本地选项。这个功能本身在帮助文件里说得挺清楚，我就不重复了。

在上面图里的那个文本文件中，我使用了下面的模式行：

vim:set et sts=2 tw=68 com-=mb\:* com+=fb\:* fo=tcqaw:

上面这个模式行，除了设定了我们讨论过的 et 扩展 tab 为空格选项、sts 软 tabstop 选项、tw 行宽选项和 fo 格式化选项外，对 comments（缩写 com）选项进行了调整，不用星号作为注释中间部分的开始（mb），而用星号作为一个列表的开始（fb）。关于 comments 选项的详细解释可以参看帮助文档 :help format-comments，我这儿就不展开了。

拼写检查

写英文时启用自动拼写检查，这是一个写作的好习惯。哪怕你英语很好，也可能因为疏忽而拼错。Vim 从版本 7 开始，就内置了拼写检查的功能，可以通过选项 spell 来打开。

开启了拼写检查的 Vim

上面我存心写错了一个地方（“userz”），被 Vim 用红波浪线标了出来。标蓝波浪线的是 Vim 提醒我，句首一般需要首字母大写（此处没有错误）。另外，可以注意到单个换行是不会被 Vim 当作一段结束的，因此行首的“right”和“promise”等单词不会被 Vim 当成有拼写问题。

在 macOS 和 Windows 的图形界面 Vim 里，右键默认就可以弹出拼写纠正菜单，跟大部分其他有拼写检查功能的软件差不多。在 Linux 上，右键默认是 xterm 标准的扩展选择区域的行为。要让右键在 Linux 下也能弹出菜单，你需要手工在 vimrc 配置文件里加入：

set mousemodel=popup_setpos

这可以算是图形界面的 Vim 跟终端 Vim 比起来的明显优势了：终端 Vim 显示不了波浪线，对右键的响应也通常有问题。在终端 Vim 里，你可能就需要去记拼写检查的命令了（见帮助 :help spell）。

Vim 拼写检查的默认语言是英语。对不同语言的支持，我们可以使用 Vim 选项 spelllang 来设定。比如，如果你希望按照英式英语的拼写，那你可以设置：

set spelllang=en_gb

这样一来，Vim 就会把非英式英语的拼写方式用绿色波浪线（当然，这和色彩方案有关）标出来。在图形界面 Vim 里，你同样可以很方便地用右键点击来更改：

标出不在 spelllang 里面的拼写方式和右键菜单

你如果希望使用美式英语，当然使用 :set spelllang=en_us 就可以了。同时使用英式和美式，可以用 :set spelllang=en_gb,en_us。如果任何英语拼写都能接受，那使用默认值或 en 就行。

作为东亚文字的特殊情况，如果你希望所有的东亚字符不被标成拼写错误的话，可以在 spelllang 选项里使用特殊值 cjk。作为中国人，我们可能会需要这么用：

set spelllang+=cjk

拼写完成

我们之前提到了 Vim 支持用一个字典文件来进行拼写完成，你可以在帮助 :help 'dictionary' 里找到相关的信息。不过，这种方式需要你手工去寻找一个字典文件，并在 vimrc 里进行配置，不那么方便。更简单的方式，是先启用拼写检查，然后正常使用拼写完成的快捷键 <C-X><C-K> 即可。如果你完整拼出了单词，但 Vim 提示拼错了，你也可以使用快捷键 <C-X>s 来使用和查看拼写建议。

注意：虽然 <C-S> 在图形界面 Vim 里可以使用，但终端（不是 Vim）可能会解释 <C-S> 为特殊控制字符，因而我们一般不使用 <C-X><C-S>。如果你发现一不小心键入 <C-S> 导致终端表现得像失去响应一样，一般可以用 <C-Q> 来恢复。

中文文本编辑

跟英文文本编辑类似，中文处理同样有段中有断行和段中无断行两种方式。如果段中有断行，中文的主要处理麻烦是在转换成 HTML 或其他格式时通常不应该把行尾结束符转换成空格。我目前测下来，GitHub 的 Markdown 能有这样的合理行为，但很多其他工具，如 pandoc，则没有对中文作这样的特殊处理。因此，中文文档还是不在段中进行断行更保险一些。

这两种文本组织方式 Vim 都是能处理的，其方式和英文文本编辑差不多，差别主要表现在下面两点。

首先，英文段中不分行时我们推荐使用 linebreak 选项，但中文段中不分行时我们则不推荐启用这个选项。因为让 Vim 挑空格位置来折行，反而会让中文文本显得乱、不好看。下面的图展示了区别：

使用选项 nolinebreak 的效果

使用选项 linebreak 的效果

其次，Vim 在处理中日韩（CJK）文字时在格式化选项 formatoptions 里是有些特殊设置的。我们重点关注下面 4 个：

m：可以在任何值高于 255 的多字节字符上分行。这对 CJK 文本尤其有用，因为每个字符都是单独的单位。

M：在连接行时，不要在多字节字符之前或之后插入空格。优先于 B 标志位。

B：在连接行时，不要在两个多字节字符之间插入空格。有 M 标志位时无效。

]：严格遵循 textwidth 选项。当设定这个标志时，除非断行禁则使得行长不可能保留在限定的文本宽度以内，行长不允许超出限定的文本宽度。这个选项主要用于 CJK 文字，并且仅在 encoding 是 utf-8 时才生效。

如果文档的主语言是中文（或日文、韩文），那 m 肯定是需要设置的，这样才能在中文之中断行。M 和 B 一般我们也会设置其中一个，取决于行文规则，在中文和西文字符之间是否手工插一个空格。如果不插空格，那就用 M；如果用空格（就像本文一样），那就用 B。由于 Vim 不区别汉字和汉字标点，使用 B 时会导致全角标点前后出现空格，我一般仍然使用 M 而不是 B。

标志 ] 是和 CJK 断行规则一起在 Vim 8.2.0901 这个版本引入的。你可以通过下面的截图看下这个标志的效果：

不同 Vim 版本和选项对中文排版的影响

换句话说，默认未使用 ] 标志时，Vim 的行为是允许 CJK 标点符号突出到 textwidth 限定的宽度以外；使用 ] 标志则不允许这样的特殊处理。你可以按照个人喜好和文本类型来酌情使用这个选项。

内容小结

在这一讲，我们讨论了使用 Vim 进行文本编辑的一些要点，重点是对 Markdown 文件进行编辑。学过这讲之后，我们应当记住：

中英文文本编辑通常有段中换行和段中不换行两种惯例，英文文本使用段中换行居多，而中文文本使用不换行的比例要高得多。

Vim 的 textwidth 选项用来设置一行文本的最大（半角）字符数，对于英文，72 是一个常见的数值。

Vim 的 formatoptions 选项可以设置很多如何对文本进行格式化的标志，很多文件格式插件（ftplugin）会对其进行设置。

模式行可以用来记录只对单个文件生效的 Vim 选项；如果文件有特殊的格式化要求，可以写在模式行里。

Vim 提供了拼写检查和拼写完成功能，可以用于英文等字母文字。

如果使用段中不换行的惯例，则英文文本应当使用 linebreak 选项，但中文文本不应当使用该选项。

在处理中文文本时，m、M、B 和 ] 是可能有用的特殊 formatoptions，应当根据实际需要使用。

本讲我们的配置文件有几处改动，对应的标签是 x1-unix 和 x1-windows。

课后练习

请尝试写一个小小的英文 README.md，内容不限，可以从其他地方复制。要求是：

内容除标题外至少三段，每段至少两行

使用模式行控制选项

段中分行，启用自动格式化但不使用行尾空格的方式

尝试在该文件中嵌入代码

想一想，为什么 a 选项一般和 w 选项一起使用

你在写 README 文件，或者其他文本文档时，还遇到过其他问题吗？请及时和我交流。你也可以把今天这一讲的内容，分享给身边其他需要编辑纯文本文件的朋友。

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

拓展 2｜C 程序员的 Vim 工作环境：C 代码的搜索、提示和自动完成

你好，我是吴咏炜。

从今天开始，我们会用连续 3 讲，深入讨论怎么为编程定制环境。如果你是 C 程序员，那么今天这一讲对你来说毫无疑问是必修课。如果你用的是类 C 语言，也能从这一讲中学到很多有用的内容，尤其是在语法加亮精调、tags 和 Clang-Format 部分。

语法加亮精调

在第 4 讲中我们已经学到了，Vim 能根据文件类型对代码进行加亮。在第 12 讲里，我们还进一步讨论了 Vim 实现语法加亮的细节，知道这些是如何通过代码来进行控制的。对于 C（含其他基于 C 的语言如 C++），语法加亮文件有一些精调选项，还挺有意思，能应对一些特殊场景的需求。我一般会设置以下几项：

let g:c_space_errors = 1
let g:c_gnu = 1
let g:c_no_cformat = 1
let g:c_no_curly_error = 1
if exists('g:c_comment_strings')
  unlet g:c_comment_strings
endif

第一项 c_space_errors 用来标记空格错误，包括了 tab 字符前的空格和行尾空格，这样设置之后 Vim 会把这样的空格加亮出来。

第二项 c_gnu 激活 GNU 扩展，这在 Unix 下一般是必要的。

第三项 c_no_cformat 不对 printf 或类似函数里的的格式化字串进行加亮。这条可能看个人需要了。我是对错误的加亮超级反感，所以关闭这种不分场合的加亮。

第四项 c_no_curly_error 也是为了让一些 GNU 的扩展能够正确显示，不会被标志成错误。

最后，Vim 默认会在注释中加亮字符串和数字（c_comment_strings）。虽然这种加亮有时候也能派上用场，但这种配置下我常常在注释中见到错误的加亮，所以我还是关闭这个功能。

关于这些选项的说明，以及其他的 C 语法加亮选项，你可以查看帮助 :help ft-c-syntax。

Tags

我们在第 8 讲里已经讨论过了 tags 文件。回想一下，tags 是 ctags 工具分析你的代码之后生成的可读文本符号数据库，帮助你找到一个符号的定义所在位置。这个 tags 格式有点简单、有点平（tǔ），却也正好像极了 C 语言的特点：简单，平淡，然而强大。

C 是一种比较简单的语言，近些年来没有什么大变化。因而如果你能安装 Universal Ctags 固然好，但很多包管理器里自带的 Exuberant Ctags 也基本够用了。本讲中的内容只要求 Exuberant Ctags。

推而广之，本讲我们用到的工具都比较简单，且容易获得。这实际上就是我把 C 和 C++ 分开的原因——要处理好 C++，需要的工具就会复杂不少。

回到 tags。Exuberant/Universal Ctags 有大量的命令行参数，如果我们要对某个目录下的所有 C 代码生成 tags 文件，我们可以使用：

ctags --languages=c --langmap=c:.c.h --fields=+S -R .

这儿我用了 --languages 选项来指定只检查 C 语言的文件；同时，因为 .h 文件默认被认为是 C++ 文件，所以我使用 --langmap 选项来告诉 ctags 这也是 C 文件。而 --fields=+S 的作用，是在 tags 文件里加入函数签名信息。我们后面会看到这类信息的作用。

这样生成的 tags 文件只考虑符号的定义，而不考虑符号的声明。对于大部分项目，这应该是合适的。如果你希望 Vim 能跳转到函数的声明处，则需要加上 --c-kinds=+p，让 tags 文件包含函数的原型声明。但是这样一来，一个函数就可能有原型声明和实际定义这两个不同的跳转位置，所以通常你不应该这样做。我只对系统的头文件生成 tags 文件时使用 --c-kinds=+p。

此外，一个可能的麻烦就是 tags 文件需要进行更新。对于一个更新不频繁的项目，最简单的方式就是在开始时运行一下上面的命令，然后就一直不管了。而对于活跃开发中的项目，我们需要更好的办法。

一个最简单的办法，显然就是在文件存盘后自动运行上面这样的命令。比如：

function! RunCtagsForC(root_path)
  " 保存当前目录
  let saved_path = getcwd()
  " 进入到项目根目录
  exe 'lcd ' . a:root_path
  " 执行 ctags；silent 会抑制执行完的确认提示
  silent !ctags --languages=c --langmap=c:.c.h --fields=+S -R .
  " 恢复原先目录
  exe 'lcd ' . saved_path
endfunction

" 当 /project/path/ 下文件改动时，更新 tags
au BufWritePost /project/path/*  call
      \ RunCtagsForC('/project/path')

但这种方式对于大项目是不可行的，因为会在文件存盘时引入不可接受的时延。还好，Ctags 支持用 -a 选项对 tags 文件做渐进式更新。把上面的后两段代码换成下面这样就可以了：

function! AppendCtagsForC(file_path)
  let saved_path = getcwd()
  exe 'lcd ' . a:root_path
  exe 'silent !ctags --languages=c --langmap=c:.c.h --fields=+S -a '
        \. a:file_path
  exe 'lcd ' . saved_path
endfunction

au BufWritePost /project/path/*  call
      \ AppendCtagsForC('/project/path/', expand('%'))

在执行很多 Vim 命令时，可以用 % 指代当前文件；而在调用函数时，就得用 expand 函数了（可查看帮助文档 :help expand()）。代码中的其他内容你应当可以理解了。

如果你觉得上面这样做太麻烦的话，有个好消息是已经有人把类似于上面、但更加完善的功能做成了插件。插件 ludovicchabant/vim-gutentags 可以完成 tags 文件的管理工作。如果你希望 tags 文件能自动更新的话，这个插件很可能可以满足你的所有需求：基本上你只需要配置它的选项，而不需要自己写 Vim 脚本代码。

不过，如果你在一个大项目上工作，代码很少发生结构性更动的话，也许在 shell/cron 里定期执行 ctags 命令，是个最简单、最不对开发者造成干扰的选项。毕竟，靠编辑器触发 ctags 命令也不那么可靠——因为很多其他操作（比如 git 切换分支）之后，你也同样需要更新 tags。

EchoFunc

记得我们刚刚讲过在 tags 文件里加入函数签名信息吧？下面我们就要用上了。EchoFunc 插件可以用来回显函数的原型。

首先我们需要安装 EchoFunc，使用包管理器安装 mbbill/echofunc 即可。然后，如果你有正确的 tags 文件，现在当你输入函数名加 ( 时，Vim 就会在屏幕底部自动提示函数的原型了：

EchoFunc 效果示意

上图中实际上表现了 EchoFunc 的两个效果：一个是我们说的屏幕底部的原型回显，还有一个是鼠标移到符号上的气泡显示。后一个功能默认也是开启的（需要图形界面），但可以通过在 vimrc 配置文件中加入 let g:EchoFuncAutoStartBalloonDeclaration = 0 来禁用。

此外，当一个函数有多个原型声明时，可以用 Alt-= 和 Alt-- 键来进行切换。但在 Mac 上，我和明白当年选择了把 Alt-- 改成了 Alt-Shift-=。原因我现在想不起来了，估计是因为在 Mac 键盘上 Alt-- 会产生短破折号（“–”），我们写文档时仍然可能会用到；而 Alt-= 和 Alt-Shift-= 产生的分别是不等号（“≠”）和正负号（“±”），基本上不会在 Vim 里使用。

Cscope

虽然 tags 是一个很有用的工具，但在我们常见的代码跳转操作里它只做到了一半：可以通过符号名称（从使用的地方）跳转到定义的地方，但不能通过符号名称查找所有使用的地方。这时候，我们有两种基本的应对策略：

使用搜索工具（第 8 讲讨论过的 :grep）

使用专门的检查使用位置的工具

诚然，第一种方式很常用、也很通用，但这毕竟是一种较“土”的办法，其主要缺点是容易有误匹配。第二种方法如果能支持的话，在完成大部分任务时会比第一种方法优越。

对于 C 代码，我们有这样的现成开源工具。由于 Vim 直接内置了对 Cscope 的支持，因此我们今天就讨论一下 Cscope。

根据 Cscope 的文档，它的定位是一个代码浏览工具，最主要的功能是代码搜索，包括查找符号的定义和符号的引用，查找函数调用的函数和调用该函数的函数，等等。查找引用某符号的地方、调用某函数的地方、包含某文件的地方，就是 Cscope 的独特之处了。

安装和配置

用 Cscope 之前，我们首先得把它装起来。我们先要安装 Cscope 本身：

Ubuntu 下可使用 sudo apt install cscope

CentOS 下可使用 sudo yum install cscope

macOS 下可使用 brew install cscope

Windows 下可在 cscope-win32 项目里下载 cscope.exe 的可执行文件，然后放到 PATH 设定里的某一个目录下即可

然后我们需要映射使用 Cscope 的按键。Cscope 的网站上提供了一个映射的脚本，不过，它里面用到了 <C-Space>，这个快捷键在很多系统上是会有问题的。因此我替换了一下，你可以安装 adah1972/cscope_maps.vim 来使用它。现在：

使用 <C-\> 加 Cscope 命令是在当前窗口里执行 Cscope 命令

使用 |（Shift-\）加 Cscope 命令是横向分割一个窗口来执行 Cscope 命令

使用 || 加 Cscope 命令是纵向分割一个窗口来执行 Cscope 命令

Cscope 也可以配合 quickfix 窗口使用，这样，相应命令的结果就会放到 quickfix 窗口里，而不是直接在界面上提供一个可能有几千项的列表让你选择。一般推荐在 vimrc 配置文件里加入：

set cscopequickfix=s-,c-,d-,i-,t-,e-,a-

我们就采用这样的设定。

创建 Cscope 数据库

要创建 Cscope 的数据库，只要在项目的根目录下运行 cscope -b 即可。拿 Vim 的约 50 万行代码为例，在我的笔记本上首次运行该命令需要四秒，产生了一个 13M 大小的 cscope.out。后面再运行该命令，Cscope 只会更新修改的部分，那就快得多了。不过，跟 Ctags 比，不管是首次创建，还是后面更新，Cscope 都要慢一点。这当然也很正常，毕竟 Cscope 要干的事情更多。

使用

上面我们已经提到了 Cscope 命令，但我们还没有说过 Cscope 到底有哪些命令。我们可以简要列表如下：

g：查找一个符号的全局定义（global definition）

s：查找一个符号（symbol）的引用

d：查找被这个函数调用（called）的函数

c：查找调用（call）这个函数的函数

t：查找这个文本（text）字符串的所有出现位置

e：使用 egrep 搜索模式进行查找

f：按照文件（file）名查找（和 Vim 的 gf、<C-W>f 命令相似）

i：查找包含（include）这个文件的文件

a：查找一个符号被赋值（assigned）的地方

比如，我们在 Vim 的源代码里要查找 vim_free 函数的定义，只需要键入命令 :cscope find g vim_free，或者把光标移到符号 vim_free 上，然后按下 <C-\>g。在目前的配置下，使用 <C-]> 也可以，因为 cscope_maps.vim 脚本里设置了 :cscopetag 选项（参见帮助 :help cscopetag）。

如果我们想要使用分割窗口，那对应的命令是 :scscope find g vim_free，光标已经在符号上时的按键是 |g 或 <C-W>]。如果我们想要垂直分割窗口，那命令是 :vert scscope find g vim_free，光标已经在符号上时的按键是 ||g。

其他的 Cscope 命令也类似，把 g 替换成相应的命令即可。值得提一下，虽然 t 和 e 命令在 Vim 里可以用 :grep 命令替代，但这两个命令执行起来要比 :grep 快。当然，Cscope 的真正优势还是 s、c、i、a 这样的命令。下面展示的是在一个 Vim 源文件中执行符号引用查找（s）的结果，可以看到比 :grep 还是方便快捷了很多的：

在 Vim 源代码中查找一个符号的引用

ClangComplete

下面，我们来讨论一下一个新话题，C 代码的自动完成。

众所周知，Clang 是一个目前很流行的、模块化的 C/C++ 编译器。它跟其他 C/C++ 编译器最不一样的地方，是它让其他程序能够很容易利用 Clang 对源代码的处理结果。目前很多对 C/C++ 源代码进行处理的工具，都是基于 Clang 来开发的。ClangComplete 也是其中的一个，它在 Vim 中添加了对 C/C++ 代码的自动完成功能。

说到这里，我需要强调一下，ClangComplete 目前已经不是我最推荐的自动完成插件了——我更喜欢下一讲要讨论的 YouCompleteMe，它的功能更为强大，使用也更为方便。不过呢，ClangComplete 在某些环境里安装起来更加简单，如果出于某种原因，你的系统上安装 YouCompleteMe 不成功，那么 ClangComplete 也不失为一个后备方案。

此外，在我的配置方案里，ClangComplete 和 YouCompleteMe 是可以共用配置文件的，所以讲 ClangComplete 的功夫也不会完全白费。下面我就简单地介绍一下。

安装

ClangComplete 对系统的基本要求就是你已经安装了 LLVM/Clang。你需要告诉 ClangComplete，在哪里可以找到 libclang。

Windows 下默认是没有 Clang 的，如果你不是已经安装了 Clang，你可以直接跳过这节，直奔下一讲讨论 YouCompleteMe 的安装过程。

在 macOS 上，如果你安装了开发工具，那其中就应该有 libclang，通常你可以在 /Library/Developer/CommandLineTools/usr/lib 目录下找到 libclang.dylib，或者如果你用 Homebrew 安装了 llvm 的话，应该可以在 /usr/local/opt/llvm/lib 目录下找到 libclang.dylib。

在 Linux 上，这又是个跟发布版相关的问题了。我们一般可以通过关键字“libclang”、“clang”和“llvm”（从最特别到最通用）来查找。在 Ubuntu 18.04 上，我们可以在使用命令 sudo apt install libclang1-10 之后找到文件 /usr/lib/x86_64-linux-gnu/libclang-10.so.1。在 CentOS 7 上，我们可以在安装 LLVM Toolset 7.0 之后找到文件 /opt/rh/llvm-toolset-7.0/root/usr/lib64/libclang.so。这些路径我们等会儿就要用到。注意如果 libclang 的文件名不是“libclang”加平台的动态库后缀的话，我们需要使用 libclang 的完整名字。

有了 libclang 之后，ClangComplete 本身的安装很简单，就是在包管理器里安装 xavierd/clang_complete，然后在 vimrc 配置文件里加一个全局变量，告诉 ClangComplete 在哪儿可以找到 libclang。比如，在上面说的 Ubuntu 18.04 里，我们就应该在 vimrc 配置里加上：

let g:clang_library_path = '/usr/lib/x86_64-linux-gnu/libclang-10.so.1'

下面就是我们在这样配置过后，在 Ubuntu 里得到的结果：

ClangComplete 的效果

我们可以看到，现在 Vim 知道了 tm 是个结构的指针，并且知道指针的成员有哪些。

这个例子比较简单，如果我们在命令行上进行编译的话，不需要任何特殊参数。如果我们命令行上需要参数，那很可能 ClangComplete 也需要知道这些参数，才能正确工作。这些参数信息应该放在文件所在目录或其父目录下的名为 .clang_complete 的文件里。比如，我的极客时间 C++ 课程的示例代码里就有这个文件，内容也很简单：

-std=c++17
-D ELPP_FEATURE_CRASH_LOG
-D ELPP_FEATURE_PERFORMANCE_TRACKING
-D ELPP_NO_DEFAULT_LOG_FILE
-D ELPP_PERFORMANCE_MICROSECONDS
-D ELPP_UNICODE
-I common
-I 3rd-party/nvwa
-I 3rd-party/cmcstl2/include
-I 3rd-party/cppcoro/include
-I 3rd-party/expected/include

由于只要能编译就可以工作，一般这个配置文件里只需要定义语言标准、预定义宏和头文件路径就可以了，优化选项、库路径和链接库名字则不需要。

Clang-Format

Clang-Format 是又一个 Clang 项目提供的工具，能够很“聪明”地格式化你的代码。作为 Clang 家族的一部分，它的代码格式化是在基于能够真正理解语言语法的基础上做的，因此比其他的格式化工具要智能、强大得多。在你安装了这个工具之后，它很容易和 Vim 集成，能大大提升代码格式化体验，因此我在这里也介绍一下。

在 Windows 上和 macOS 上，Clang-Format 一般作为 LLVM 安装的一部分提供。Windows 用户建议直接安装官方提供的下载版本。macOS 用户一般建议使用 Homebrew 安装：brew install llvm。

Linux 上就复杂点了，取决于不同的发布版，Clang-Format 可能作为 LLVM 大包的一部分提供，也可能是一个单独的工具。比如，在 Ubuntu 里，Clang-Format 是单独安装的：sudo apt install clang-format。而在 CentOS 7 里，Clang-Format 是 llvm-toolset-7.0 的一部分。所以你需要自己检查一下。

有了 clang-format 可执行程序之后，我们还需要一个 clang-format.py 脚本来和 Vim 集成。这个脚本文件的安装位置在不同环境是不同的，而且路径可能跟 LLVM 版本相关。比如，我在 Ubuntu 下从 /usr/share/clang/clang-format-10 下面找到了这个文件，在 macOS 上则是一个固定位置 /usr/local/opt/llvm/share/clang。但如果你在 LLVM 所在的目录下找不到的话，也没关系。你可以直接从网上下载，放到你自己知道的一个位置。

我们如果把这个脚本的位置记作 /path/to/clang-format.py，那我们现在在 vimrc 配置里加上这行以后就能工作了：

noremap <silent> <Tab>  :pyxf /path/to/clang-format.py<CR>

我是映射了 <Tab> 在正常模式和可视模式下对代码进行格式化。如果正常模式的话，我们是对当前语句执行格式化。如果可视模式，那就是对选定的行进行格式化了。这些应该都非常自然了。

Clang-Format 使用规则配置文件来确定如何进行格式化。首次配置觉得复杂的话，你可以参考我的配置文件。具体的效果你可以根据你的项目要求来调整。这个配置文件 .clang-format 同样是放在你的源代码文件目录下或其某一父目录下。

格式化的过程可以参考下面的动画：

在 Vim 里用 Clang-Format 格式化代码

内容小结

这一讲我们讨论了 C 程序员应当如何在软件项目配置 Vim，使得开发和浏览更为便利。我们主要讨论了下面各项：

Vim 的 C 语法加亮有一些选项，可以精调来满足一些特殊需求。

Vim 的 tags 支持使我们可以快速地跳转到符号的定义处。我们也可以配置 Vim 来自动更新 tags 文件。

EchoFunc 插件可以让我们更容易查看函数的原型。

Cscope，更近一步，让我们可以飞快地找到符号的使用位置、函数在哪儿被调用等等这样的信息。

ClangComplete 依托 libclang 对 C/C++ 代码的解析能力，可以让我们在 Vim 里得到对 C/C++ 代码的自动完成功能。

Clang-Format 依托 libclang 对 C/C++ 代码的解析能力，可以通过规则非常智能地对 C/C++ 代码进行重新格式化。

课后练习

卖油翁说：“无他，唯手熟尔。”这句话对开发来说是绝对适用的，对使用编辑器也同样如此。今天讨论的功能和插件，没什么难的，唯用而已。对于语法加亮精调、tags、EchoFunc、ClangComplete 和 Clang-Format，只要配置好了，后续使用是不需要费任何力气的，可以先搞好。对于 Cscope，它的按键需要记忆，建议对照我给出的英文，多试几次，能形成按键直觉之后，使用也会很轻松。

请把这些插件都装好，使用一下。对于 Cscope，尤其需要在你的 C 项目中实际操练几下，做到真正会用和真正去用。如果遇到任何问题，欢迎留言和我交流。

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

拓展3｜Python 程序员的 Vim 工作环境：完整的 Python 开发环境

你好，我是吴咏炜。

今天这一讲，我会介绍 Python 程序员定制 Vim 工作环境的完整方法。

Python 的流行程度越来越高，Python 程序员们对此一定是很高兴的。在 Stack Overflow 的 2020 年开发者调查里，Python 在最受爱戴（most loved）的语言里排名第三，而在最想要（most wanted）的语言里则已经连续四年排名第一！因此，它在 Vim 的生态系统里受到了良好的支持，也不会是件令人吃惊的事。有开发者已经把 Python 开发所需要的若干插件揉到了一起，组成了一套开箱即用的工具，python-mode。

今天我们就以它为基础，讨论一下 Vim 对开发 Python 提供的支持。

功能简介

Python-mode 实际上是以 Vim 插件形式出现的一套工具，它包含了多个用于 Python 开发的工具。根据官网的介绍，它的主要功能点是：

支持 Python 3.6+

语法加亮

虚拟环境支持

运行 Python 代码（<leader>r）

添加 / 删除断点（<leader>b）

改善了的 Python 缩进

Python 的移动命令和操作符（]], 3[[, ]]M, vaC, viM, daC, ciM, …）

改善了的 Python 折叠

同时运行多个代码检查器（:PymodeLint）

自动修正 PEP 8 错误（:PymodeLintAuto）

自动在 Python 文档里搜索（K）

代码重构

智能感知的代码完成

跳转到定义（<C-c>g）

……

不过，还是要提醒一句，它的功能虽然挺多，但作为非商业软件，全靠志愿者来贡献代码，并不是所有功能的完成度都很高。有些功能做得尚不完善，有些功能则略显鸡肋，所以，我也不会全部都讲解。我们就择善而从之，在利用它不需要用户介入就能提供的功能外（如语法加亮和缩进），重点讲解它做得好的地方，以及可能有陷阱需要规避的地方。

安装

Python-mode 没有编译组件，全部由脚本代码组成，因而使用你的包管理器安装 python-mode/python-mode 即可，非常简单。

以 minpac 为例，你只需要在 vimrc 配置文件中“Other plugins”那行下面加入：

call minpac#add('python-mode/python-mode')

然后执行 :PackUpdate 命令即可。

配置

在没有任何配置的情况下，python-mode 也是完全可用的。但如果你再做一些基本设置的话，就能够解决一些常见问题和规避一些常见陷阱。

我个人的设置是下面这个样子的：

function! IsGitRepo()
  " This function requires GitPython
  if has('pythonx')
pythonx << EOF
try:
    import git
except ImportError:
    pass
import vim

def is_git_repo():
    try:
        _ = git.Repo('.', search_parent_directories=True).git_dir
        return 1
    except:
        return 0
EOF
    return pyxeval('is_git_repo()')
  else
    return 0
  endif
endfunction

let g:pymode_rope = IsGitRepo()
let g:pymode_rope_completion = 1
let g:pymode_rope_complete_on_dot = 0
let g:pymode_syntax_print_as_function = 1
let g:pymode_syntax_string_format = 0
let g:pymode_syntax_string_templates = 0

稍微解释一下：

IsGitRepo 是利用 Python 代码检测当前是不是在 Git 库目录下的一个函数，它要求你在 Python 环境里安装了 GitPython（pip3 install GitPython）。

我们仅仅在当前目录是一个 Git 库下面才启用 rope 支持（pymode_rope）。Rope 是 python-mode 里提供语义识别和自动完成的主要工具，它会扫描所有子目录并创建 rope 工程目录。如果你一不小心在你的主目录（或子目录非常多的地方）执行 python-mode 的命令，可能会导致 Vim 卡顿（python-mode 并不是一个异步的插件）。所以我们在这儿特别限制一下，防止误操作。

我们启用 rope 的完成功能（pymode_rope_completion）。

我们禁用在输入 . 号时自动完成的功能（pymode_rope_complete_on_dot）。这是因为 rope 提供的自动完成会侵入式地影响正常输入流，即如果我想不理睬自动完成是不行的。这一点就不如 YCM 了。因此，我们的自动完成仍然使用 YCM。不过，需要的话，我们仍可以通过 <C-X><C-O> 来使用 rope 的自动完成。

Python-mode 对 Python 语法的加亮改善还不错，但它的默认行为是把 print 作为保留字显示，而不是普通函数。在写 Python 3 时，还是需要修改一下它的行为（pymode_syntax_print_as_function）。

Python-mode 会试图对字符串中出现的格式化字串和模板替换字串做特殊的加亮（pymode_syntax_string_format 和 pymode_syntax_string_templates）。这儿主要的问题是，它会误匹配字符串中出现的 {} 和 $ 序列。我个人不习惯错误的加亮，不过你可以根据自己的喜好，来决定是不是要启用这个功能。

使用

语法加亮

Python-mode 提供了自己的语法加亮文件。除了上面提到的可以选择对 print 如何加亮，以及在字符串内部进行特殊加亮的选项外，它还提供了很多改进，并且可以由用户通过选项来微调（:help pymode-syntax），如对赋值号（=）的特殊高亮和对 self 的特殊高亮，等等。这些改进我觉得还挺有用。

代码折叠

我个人一直不怎么喜欢代码折叠（主要是觉得额外展开这个步骤非常有干扰，而更愿意一目十行式地快速浏览），所以 Vim 的这个功能我基本不用。如果你喜欢折叠的话，你应该会很高兴 python-mode 能帮你自动折叠 Python 代码。你只需要在 vimrc 配置文件中加入下面这行即可：

let g:pymode_folding = 1

效果见下图：

代码折叠效果

这个功能会导致打开 Python 文件变慢。你可以试试，斟酌一下自己是否希望使用这个功能。

快速文档查阅

Python-mode 默认映射了 K 对光标下的单词进行文档查阅。跟其他查阅文档的方式比起来，这还是非常快捷方便的。

使用 K 查看 Python 的文档

缩进支持

在 Vim 的运行支持文件中，本来就包含了对 Python 缩进的支持，但默认的支持并没有把像 PEP 8 这样的 Python 编程规范考虑进去，缩进风格并不十分正确。安装了 python-mode 后，缩进就能更好地自动遵循 PEP 8 规范了。

代码检查

不管 Vim 的缩进对不对，如果你在其他编辑器里编辑了 Python 代码，Vim 是不会修正其中的缩进或其他问题的——除非你启用代码检查器。

Python-mode 里带了好几个代码检查器，默认启用的是下面三个：

pyflakes，一个很轻量的代码检查器，检查常见的 Python 编码问题，如未使用的变量和导入

pep8，一个专门检查代码是否符合 PEP 8 的检查器

mccabe，一个专门检查圈复杂度的代码检查器

默认启用哪些检查器，是通过下面的全局变量来控制的：

let g:pymode_lint_checkers = ['pyflakes', 'pep8', 'mccabe']

你可以自己在 vimrc 配置文件里定义这个变量，调节希望使用的代码检查器。我觉得默认的代码检查器还比较合适，因为执行真的很快，基本上可以在执行检查的瞬间帮你检查完代码并标记出问题。你可以手工执行 :PymodeLint 来检查代码，python-mode 也会自动在你保存文件时进行检查。

我几年前写的不符合 PEP 8 的代码存盘试验

可以看到，检查的结果会在屏幕的左侧标记出来，表示不同的问题类型；并且光标移到这样的行上，Vim 底部还会显示问题的描述信息。同时，python-mode 检查出问题时会自动打开一个位置列表，我们在第 13 讲提过，这是跟窗口关联的类似于快速修复窗口的信息窗口。由于我们可能在多个窗口 / 标签页编辑多个文件，位置列表确实比较合适。当 python-mode 认为你修复了所有问题时，这个位置列表也会自动关闭。

顺便提醒你注意一下屏幕右侧的红线（在某些配色方案里可能是其他颜色）。这条线在第 80 列上，也是提醒你写代码不能到那个位置，因为 PEP 8 规定 Python 代码行最长是 79 个字符。如果到达红线位置的话，那 pep8 检查的时候，一定跑不了，会报错的。

上面图中的错误都是 PEP 8 问题，绝大部分可以简单地执行 :PymodeLintAuto 命令来自动解决，用不着我们自己去动手修改代码。

Python-mode 还有两个没有默认启用的检查器：

pylint，一个功能很强的代码检查器，它可以嗅出你的代码中的坏味道，除了性能，可以说是全面强于 pyflakes（使用它你得擦亮眼睛，做好被它虐的准备）

pep257，一个检查文档串（docstring）是否符合 PEP 257 的工具（这个工具我个人感觉不成熟，给出的建议有点混乱）

由于 pylint 执行比较慢，我觉得还是先写完代码再专门来扫描并解决其报告的问题比较合适。上面的这个示例代码，跑 pylint 需要超过一秒才能执行完成，在存盘时自动执行检查基本属于不可忍受。这当然也是因为 python-mode 没有异步执行外部命令造成的。我们最后还会再看一下执行慢和异步的问题。

Rope 支持

Rope 是一个 Python 库，提供对 Python 代码的分析、重构和自动完成功能。由于我们使用 YCM 来进行自动完成，也能完成像跳转到定义这样的任务，rope 就略显鸡肋了。不过，它有重命名重构功能，而 YCM 并不支持对 Python 的重命名重构，所以两者功能还不算完全重叠。

你如果决定要用一下 rope 的话，需要了解以下几点：

rope 会使用一个叫做 .ropeproject（默认名字）的目录，在里面缓存需要的信息；这个目录在当前目录下，或当前目录的一个父目录下；如果找不到，默认会在当前目录下创建这个目录

使用命令 :PymodeRopeNewProject 路径 可以在指定路径下创建这个 .ropeproject 目录

使用命令 :PymodeRopeRegenerate 可以重新产生项目数据的缓存

默认情况下（g:pymode_rope_regenerate_on_write 等于 1），在文件存盘时 python-mode 即会自动执行 :PymodeRopeRegenerate 命令

在启用 rope 之后，你就可以使用下面的命令了：

使用 <C-X><C-O> 来启用自动完成（我们把 . 还是交给 YCM 了）

使用 <C-C>g 来跳转到定义（跟 YCM 的 \gt 比，大部分情况下没区别；rope 跳转更好和 YCM 跳转更好的情况都有，但都不多见）

使用 <C-C>d 来查看光标下符号的文档；和 K 键不同，这个命令可以查看当前项目代码里的文档字串

重构（refactor）功能以 <C-C>r 开始，如 <C-C>rr 是重命名（rename）光标下的符号，这些功能还是比较强大的（可以使用 :help pymode-rope-refactoring 来查看完整的帮助信息）

下面的动图展示了 rope 的若干功能：

在 rope 里查看文档、跳转到定义和重命名

替换方案

如果你对 python-mode 的某些功能不满意，可以禁用其部分功能，用其他插件来代替。

首先，如果你如果觉得 rope 提供的额外功能对你用处不大的话，我们可以完全禁用 rope（let g:pymode_rope = 0），专心使用 YCM。这样，硬盘上也就不会出现 .ropeproject 那样的目录了。

其次，如果你真的希望能在写代码的时候自动进行 pylint 检查，那你也可以禁用 python-mode 里的代码检查器功能（let g:pymode_lint = 0），转而使用 ALE 来进行异步检查。你需要安装它（包管理器需要的名字是 dense-analysis/ale），并在 vimrc 配置文件中加入：

let g:ale_linters = {
      \'python': ['pylint'],
      \}

别忘了这种情况下，你需要自己用 pip 安装 pylint。这不像 python-mode 的情况，所有工具都已经打包在那一个套件里面了。

内容小结

在这一讲，我们通过介绍 python-mode，介绍了一个比较适用于 Python 程序员的 Vim 开发环境。这个工具集成了对 Python 的语法加亮、代码折叠、文档查阅、代码检查、自动完成等多方面的功能，对 Python 开发者非常适用。我们同时也讨论了 Vim 之外的一些代码检查工具，以及当你对 python-mode 不满意时，如何部分替换其功能。

课后练习

同样地，学完今天这一讲之后，你的主要任务就是把 python-mode 装起来、配置好、用一下。如果遇到什么问题，欢迎留言和我讨论。

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

拓展4 | 插件样例分析：自己动手改进插件

你好，我是吴咏炜。

上一讲，我们对 Vim 脚本作了初步的介绍。Vim 脚本可以应用在很多不同的场景下，比如我们的 vimrc 配置文件和配色方案也都是 Vim 脚本。但我们今天更关心的，是我们经常使用的、一般称为“插件”的特殊 Vim 脚本。虽说插件和其他 Vim 脚本之间的界限也并非泾渭分明，但我们一般把满足以下条件的 Vim 脚本称为插件：

功能独立，不依赖特殊的个性化配置

存在加载时即会执行的代码，一般放在 plugin 目录下（也有放在 ftplugin、ftdetect 等其他目录下的情况）

今天，我们就利用目前学到的知识，来由浅入深地分析四个样例，了解插件代码是如何编写的，从而可以自己动手定制、改进，进而编写自己的插件，真真正正地定制自己的 Vim 环境，并为整个 Vim 社区作出贡献。

ycmconf

我们要看的第一个脚本，是在讲 YCM 时引入的 ycmconf。这是一个非常简单的插件，我们就拿它开始我们今天的课程。

如果你之前按我说的步骤安装的话，现在应该可以在 Vim 配置目录下的 pack/minpac/start/ycmconf 里找到它。你也可以自己用 Git 签出：

git clone https://github.com/adah1972/ycmconf.git

除去一些文本说明文件，这个插件里只有两个真正的脚本文件：

plugin/ycmconf.vim

ycm_extra_conf.py

plugin 目录是 Vim 里主要放置无条件执行的脚本的地方，即“插件”的主体。打开 plugin/ycmconf.vim，我们看到里面只有一行注释加一行代码：

" Set the global extra configuration
let g:ycm_global_ycm_extra_conf=expand('<sfile>:p:h:h') . '/ycm_extra_conf.py'

这个差不多是个最简单的插件了吧。Vim 脚本里只做了一件事，设置全局变量 g:ycm_global_ycm_extra_conf 给 YCM 用。关于脚本中的 expand 函数，我们稍微展开一下：

expand 是用来展开文件名的。参数字符串里如果有 %，就代表当前编辑的文件名；如果有 <sfile>，代表当前执行的源代码文件（其他可展开的名字请参见 :help expand()）。

:p 用来告诉 expand，我们需要得到完整的路径。比如，在我的机器上，这样展开的结果是 /Users/yongwei/.vim/pack/minpac/start/ycmconf/plugin/ycmconf.vim。

:h 用来告诉 expand，我们需要头部，即去掉路径的最后部分。我会得到 /Users/yongwei/.vim/pack/minpac/start/ycmconf/plugin。

第二次使用 :h，我们再次去掉路径的最后部分，即 plugin。我会得到 /Users/yongwei/.vim/pack/minpac/start/ycmconf。

随后，我们拿这个路径跟 '/ycm_extra_conf.py' 进行拼接，就得到了 YCM 可以看到的 ycm_extra_conf.py 文件的路径。

这个插件的主体功能在 ycm_extra_conf.py 里。鉴于这是 Python 的代码，而不是 Vim 脚本，我就不再讲解了。你如果有兴趣的话，可以自己看一下。文件虽然总共要好几百行，但注释比较多，逻辑其实不复杂；如果你懂 Python，一定可以理解里面的内容。

cscope_maps.vim

你一定觉得，上面这个脚本实在也太太太简单了吧……

下面我们就来看一个稍复杂点的。拓展 2 里我给出了一个自己改过的 cscope_maps.vim，我们现在就来看看它的原始版本，然后看一下怎么修改它的行为。

原始版本在 Cscope 的网站上。可以看到，这也是一个比较简单的 Vim 脚本，应当直接放到 plugin 目录下。虽然文件总共有一百多行，倒有一大半是注释；实际代码行只有三十几行。我们可以细细地分析一下：

最外围，是一个条件语句，确保这个插件的内容仅在 Vim 支持 Cscope 时得到执行：

if has("cscope")
    …
endif

在条件语句里，有三行是设置 Vim 选项的：

set cscopetag
set csto=0
set cscopeverbose

我们在 Vim 帮助里可以查到它们的详细说明。简单来说：

设置 cscopetag 使得我们在使用原先 tags 相关的命令时会同时查找 Cscope 数据库

设置 csto 为 0 是让 Vim 先查找 Cscope 数据库，找不到才查找 tags

设置 cscopeverbose 是让 Vim 在之后添加 Cscope 数据库时，告诉你结果成功与否

设置最后这个选项是在下面的语句之后：

if filereadable("cscope.out")
    cs add cscope.out
elseif $CSCOPE_DB != ""
    cs add $CSCOPE_DB
endif

也就是说，Vim 会在启动时悄无声息地试图加载当前目录下的 cscope.out 数据库或环境变量 CSCOPE_DB 指定的数据库，并且不会报告结果。

剩下的代码就全部是……键映射了。我们就看其中的一个，其余的都大同小异：

nmap <C-\>s :cs find s <C-R>=expand("<cword>")<CR><CR>

这个键映射把 <C-\>s 映射成了一个 :cs find s … 命令。值得注意的是命令的后半截：

脚本里使用 <C-R>=…<CR> 来执行一个 Vim 表达式，并把结果填到命令里。

我们又一次见到了 expand 函数。这一次，要展开的是 <cword>，即当前光标下的单词。

注意结尾两个 <CR> 里第一个是给 <C-R>= 的，第二个才是执行命令的回车键。

让我有意见的是下面这样的键映射：

nmap <C-@>s :scs find s <C-R>=expand("<cword>")<CR><CR>

这儿用 <C-@> 代表 Ctrl- 空格，而这个组合键在很多系统上不可用。既然已经使用了 Ctrl-\ 作为 Cscope 的专用起始键，我觉得继续用 Shift-\ 就好。由于 | 在 Vim 里用来分隔多个语句，这儿我们要换个写法，改成：

nmap <bar>s :scs find s <C-R>=expand("<cword>")<CR><CR>

我的完整修改过程，可以查看：

https://github.com/adah1972/cscope_maps.vim/commits/master

总的来说，这也是个非常小、非常轻松的修改。

EchoFunc

事实上，大部分行为良好的插件会允许用户通过一些全局变量来定制键映射之类的设定。不过，对于没有提供这种定制性的插件，我们自己找到代码里的键映射语句，手工修改一下，也是一种可能发生的常见情况。比如，EchoFunc 里查看下一个和上一个函数的按键分别可以用全局变量 g:EchoFuncKeyNext 和 g:EchoFuncKeyPrev 来修改。一般来说，插件的文档里会进行说明，你也可以在插件里通过搜索 exists 函数来找到插件提供出来的定制点。

以 EchoFunc 为例，它虽然简单到没有提供帮助文档，但插件的主文件（after/plugin/echofunc.vim）开头有大段的注释。同时，它有大量的 exists 的函数调用，来检查用户是否已经定义了某一全局变量来定制行为：

if !exists("g:EchoFuncMaxBalloonDeclarations")
    let g:EchoFuncMaxBalloonDeclarations=20
endif

if !exists("g:EchoFuncKeyNext")
    if has ("mac")
        let g:EchoFuncKeyNext='≠'
    else
        let g:EchoFuncKeyNext='<M-=>'
    endif
endif

if !exists("g:EchoFuncKeyPrev")
    if has ("mac")
        let g:EchoFuncKeyPrev='±'
    else
        let g:EchoFuncKeyPrev='<M-->'
    endif
endif

在我这儿给出的三个全局变量的相关定义里，第一个是对起泡提示的数量限制，第二个是下一个函数的键定义，第三个是上一个函数的键定义。在后两个键定义里，还分平台（Mac 或非 Mac）进行了不同的设置。这些都是非常直接了当的。

如果我们在 EchoFuncKeyNext 上面按下 * 来搜索这个变量的使用，我们就会发现它们是在函数 EchoFuncStart 里被真正使用的：

if maparg(g:EchoFuncKeyNext, "i") == '' && maparg(g:EchoFuncKeyPrev, "i") == ''
    exec 'inoremap <silent> <buffer> ' . g:EchoFuncKeyNext . ' <c-r>=EchoFuncN()<cr>'
    exec 'inoremap <silent> <buffer> ' . g:EchoFuncKeyPrev . ' <c-r>=EchoFuncP()<cr>'
endif

这儿的代码说的是：

如果 g:EchoFuncKeyNext 和 g:EchoFuncKeyPrev 描述的键映射（:help maparg()）在插入模式（"i"）没有被占用（== ''）的话，那我们就执行（exec）针对当前缓冲区（<buffer>）的插入模式键映射（inoremap），让其安静地（<silent>）执行（<c-r>=）函数中的语句。

注意，在键映射中使用 <C-R>= 来执行语句是一种常用技巧。这种情况下，我们常常不是要获得函数返回的结果（所以这些函数通常返回 ''），而只是需要执行一些指定的代码，产生需要的“副作用”。在这儿，我们需要的副作用就是选择函数列表里的下一项和上一项了。

EchoFunc 算是一个中等规模的 Vim 插件，也有好几百行代码了，我们没有必要全部讲一遍。它的初始化过程比较有特点，我们看一下：

augroup EchoFunc
    autocmd BufRead,BufNewFile * call s:EchoFuncInitialize()
augroup END

也就是说，在读入文件后，或创建新文件后，才调用 s:EchoFuncInitialize() 进行初始化。

那 s:EchoFuncInitialize() 究竟做了些什么呢？看下面：

function! s:EchoFuncInitialize()
    augroup EchoFunc
        autocmd!
        autocmd InsertLeave * call EchoFuncRestoreSettings()
        autocmd BufRead,BufNewFile * call CheckedEchoFuncStart()
        if has('gui_running')
            menu    &Tools.Echo\ F&unction.Echo\ F&unction\ Start   :call EchoFuncStart()<CR>
            menu    &Tools.Echo\ F&unction.Echo\ Function\ Sto&p    :call EchoFuncStop()<CR>
        endif

        if has("balloon_eval")
            autocmd BufRead,BufNewFile * call CheckedBalloonDeclarationStart()
            if has('gui_running')
                menu    &Tools.Echo\ Function.&Balloon\ Declaration\ Start  :call BalloonDeclarationStart()<CR>
                menu    &Tools.Echo\ Function.Balloon\ Declaration\ &Stop   :call BalloonDeclarationStop()<CR>
            endif
        endif
    augroup END

    call CheckedEchoFuncStart()
    if has("balloon_eval")
        call CheckedBalloonDeclarationStart()
    endif
endfunction

我下面概要解说一下：

在 EchoFunc 自动命令组里，执行 autocmd!，清空已有的自动命令，即刚才的 call s:EchoFuncInitialize() 语句。

在 InsertLeave，离开插入模式事件里，调用 EchoFuncRestoreSettings 函数，停止函数回显。

在读入文件或创建新文件时，检查是否需要启用函数回显。

在图形界面下创建启停函数回显的菜单项。

如果 Vim 支持气泡显示，在读入文件或创建新文件时，检查是否需要启用气泡函数声明提示，并在图形界面下创建启停气泡函数声明提示的菜单项。

对当前文件，检查是否需要启用函数回显和起泡函数声明提示。

最后，如果你好奇为什么 EchoFunc 选择使用 after/plugin 目录而不是 plugin 目录，在它的 Git 日志里是有说明的：

1) fix key “(” “)” mapping conflict with other plugins:

​ first, move plugin folder into after/ folder, so that echofunc will be load after most plugins have been loaded

​ Second, if during initialization time, if it find “(” or “)” key have been mapped, it will try to append <Plug>EchofuncXX function to it.

因为它用到 ( 和 ) 作为键映射，容易和其他插件冲突，因此它会最后加载，并尽量把自己键映射补充进去。

对于插件功能本身的特殊逻辑，我就不解释啦。

arm-syntax-vim

今天最后一个插件样例，是我最近的一个实际需求。由于我写的代码需要最终跑在 ARM 平台上，我偶尔需要检查一下产生的 ARM 汇编代码。在 Vim 的默认配置下，产生的汇编代码效果不太理想，如下图所示：

使用 asm 语法类型显示的 ARM 汇编

这里最糟糕的地方是，stmfd 那行里的 {r4, lr} 居然显示成了注释？是可忍，孰不可忍！

还好，我用不着从头开始自己搞。网上略加搜索，我就找到了 ARM9/arm-syntax-vim 这个 Vim 脚本，可以获得好得多的效果，如下所示：

使用 arm 语法类型显示的 ARM 汇编

不过，这个脚本还是缺了点东西，它只包含了语法文件，不能把 GCC 产生的 .s 文件识别为它支持的 arm、armv4 和 armv5 格式。我要做的就是添加文件类型识别，让 Vim 把 ARM 的汇编文件识别成合适的 ARM 格式。

在第 8 讲讨论文件类型判断时，我已经说过，在 Vim 里后缀不是判断文件类型的唯一依据。既然我懒到不愿意在汇编文件里加帮助识别的文本，我当然也懒得去改汇编文件的后缀了。GCC 产生的汇编代码里的一些特定标识，也使得我利用文本判断变得相当容易：取决于不同的环境，汇编中一般会出现 .arch arm 和 .cpu arm 这样的明确行。

要让 Vim 进行文件类型判断，标准做法是在 ftdetect 目录下加入判断脚本。既然我们知道后缀是 .s，在这个文件中我会写入：

au BufRead *.[sS]  call arm#ft#FTarm()

为了加快 Vim 的启动速度，真正检测需要的代码一般推荐放到 autoload 目录下。这是 Vim 的专门机制，允许脚本“按需”加载，仅在用到函数的时候，才载入函数的定义（:help autoload）。在上面的写法下面，当 Vim 读入后缀为 .s 或 .S 的文件时，Vim 会自动在 autoload/arm 目录下载入 ft.vim，然后调用其中的 FTarm 函数。

下面我们来看一下 ft.vim 文件。这个文件不大，完整内容展示如下：

let s:cpo_save = &cpo
set cpo&vim

function! arm#ft#FTarm()
  let head = ' '.getline(1).' '.getline(2).' '.getline(3).' '.getline(4).
  \' '.getline(5).' '
  " Can't use setf, as we need to overrule the default filetype setting
  if matchstr(head, '\s\.arch\s\+armv4') != ''
    set filetype=armv4
  elseif matchstr(head, '\s\.arch\s\+armv5') != ''
    set filetype=armv5
  elseif matchstr(head, '\s\.arch\s\+arm') != ''
    set filetype=arm
  elseif matchstr(head, '\s\.cpu\s\+arm') != ''
    set filetype=arm
  endif
endfunction

let &cpo = s:cpo_save
unlet s:cpo_save

开头和结尾的四行属于 Vim 脚本的标准模板写法：进入脚本时保存兼容性选项（:help 'cpoptions'）的当前值，然后恢复其为默认值，免得其他地方的设置影响对脚本的解释；退出时则恢复原来保存的兼容性选项值。

中间主体部分就一个函数，做的事情也很简单，就是把文件的头五行内容拼到一起，然后看能不能找到满足条件的“.arch”和“.cpu”语句。找到的话，就设置合适的文件类型；找不到，就不做处理，留给其他的 Vim 脚本来继续判断。

这儿唯一比较特别点的地方是，一般设置文件类型推荐使用 :setfiletype 命令，它会避免重复设置，在一次 Vim 的自动事件触发过程中只执行一次。对于我们当前的目的，这是不够的：因为在我们的代码执行之前，当前缓冲区一般已经被系统的自动命令设置过类型了。具体来说，是运行支持文件里的 autoload/dist/ft.vim 里的 dist#ft#FTasm 函数。

所以，我们这儿需要强行覆盖已经设置的文件类型，用 set filetype=… 就可以做到。要注意，仅在你很有信心你设置的类型正确时才可以这么做，否则，你可能会干扰其他插件的结果。

这样，我就做到了在用 Vim 打开 GCC 产生的 ARM 汇编文件时，能自动检测并应用合适的 arm 语法。完整的代码可从 adah1972/arm-syntax-vim 下载。

内容小结

今天我们分析了四个大小不同的 Vim 脚本，并展示了常见的 Vim 脚本用法。我们可以总结一下相关的知识点：

Vim 里主要放置无条件执行脚本的目录是 plugin。

exists 、expand 和 has 恐怕是 Vim 里最重要的函数，常用法需要牢牢掌握。

好的 Vim 脚本一般会通过全局变量允许用户定制部分行为，如键映射；Vim 脚本里面通过 exists 函数来检测用户定义的全局变量。

一般来说，插件会使用自己的名字开始自己的自动命令组，这样比较便于管理，包括统一清除。

after 目录下的内容会晚于其他目录加载。

ftdetect 目录下一般放置用来检测文件类型的脚本。

autoload 目录专门放置延迟加载的脚本。

课后练习

请选择一个你常用的插件（如 nerdtree 和 undotree），分析它的主体结构，看一下它使用了哪些不同的目录，分成几个主要的模块，提供了哪些命令，又给用户留出了哪些定制点。

如果有任何的问题和想法，请留言和我交流。我们下一讲再见。

拓展5 | 其他插件和技巧：吴咏炜的箱底私藏

你好，我是吴咏炜。

上一讲我介绍了最重要的一些插件。今天这讲拓展，我们就算是查漏补缺，再分享一些我个人这些年压箱底的收藏。这些插件和技巧有新有旧，都非常好用，欢迎你挑选感兴趣的内容，纳入自己的个人收藏箱。

插件

Syntastic 和 ALE

说到代码检查插件，我最早是从 Syntastic 开始用的，然后慢慢转向了 ALE。不过，我因为主要写 C++ 和 Python，所以慢慢放弃了使用这两个插件，转而使用对这两种语言支持较好的 YCM（第 13 讲）和 Python-mode（拓展 3）。

虽然 YCM 和 Python-mode 集成的工具比较有限，比如 YCM 对 C++ 的代码检查仅限于 Clang 系列工具提供的支持，而不像 Syntastic/ALE 还可以选择很多其他的工具，但是，它们对我来讲还是够用了——何况对于 C 和 C++，要让 Syntastic 或 ALE 干活的话，大部分情况下需要配置头文件包含路径和编译选项，也是件麻烦事。

不过，对于其他语言的开发者，Syntastic/ALE 可能还是非常有用的。

先说 Syntastic。这是一个老牌的代码检查插件，其 1.0 版本发布在 2009 年。这些年下来，这个插件里积累了好几十种语言的代码检查支持，既有常见的 C、C++、Python、Java、JavaScript 等语言，也有冷门一点的 ACPI、AppleScript、Julia、VHDL、z80 汇编等语言。对于每种语言，它能自动识别已经安装的代码检查器，并在你文件存盘时自动检查代码（也可以手工使用 :SyntasticCheck 命令来检查）。要检查当前文件 Syntastic 识别到了哪些代码检查器，可以使用 :SyntasticInfo 命令；而在 ALE 中没有等价的好用命令。

Syntastic 的主要问题是它的检查是同步的，代码检查时你不能同时进行编辑工作：不但不能即输即查，而且耗时长的检查还会中断正常的编辑流程。后起之秀 ALE 恰恰解决了这个问题，它会异步地检查你的代码，在编辑时即输即查，完全不会影响正常的编辑流程。从支持的语言上来说，ALE 虽然不及 Syntastic，但也已经覆盖到了大量的冷门语言。并且，它仍然处于积极开发之中：2020 年 7 月，Syntastic 一共有 3 次提交，而 ALE 有 48 次非合并提交和 13 次合并提交！

这两个插件的配置都略显复杂，通常需要你针对每种代码检查器进行配置。因此，总体来说，我对代码检查的推荐顺序是：

使用 YCM、Python-mode、Vim-go 等有语言针对性的插件，如果你用的语言被支持，并且插件集成的代码检查功能够用的话

使用 ALE，如果你的语言和代码检查插件它能够支持的话

使用 Syntastic，如果其他选项不适用，或者你需要的检查执行够快的话

Renamer

在需要对文件进行批量更名时，我会使用 qpkorr/vim-renamer 插件。它提供 :Renamer 命令，会打开当前目录下所有文件的列表。你随后就可以利用 Vim 强大的正则表达式和编辑功能来调整这些名字了。在调整完成后，执行 :Ren 命令即可。

Renamer 的界面（额外展开了一层目录）

注意，图中的第二、三行提供了命令的说明，比如用 > 来多展开一层目录，等等。你应该只做行内的修改，而不去删除行或调整行的顺序，否则可能引致意外的后果。使用 <C-Del> 可以删除当前行的文件，这算是一种例外情况。

在图中，我只是手工修改了“log.conf”那行的文件名。更常见的情况是利用 Vim 的编辑功能做批量操作。比如，你需要把文件名变成小写，可以选中要修改的部分然后使用 gu 命令。又比如，如果你的文件里有大量的编号，你希望对编号进行增减的操作，也可以利用 Vim 的 <C-A> 和 <C-X> 来对编号进行加减操作。当然，这时你可能需要使用可视模式的列选择（<C-V>）。

你还有可能需要注意一下选项 nrformats，因为如果其中含有 octal 的话，Vim 会把 0 打头的数字序列当成八进制来处理。还好，如果你按照我目前给出的方式来设置 vimrc 配置文件的话，缺省里面不含 octal，只会对 0b 和 0x 打头的数字做特别处理，这就不会跟普通的十进制数字编号有任何冲突了。

Undowarning

Vim 里有跨会话撤销修改的功能，这当然是它的强大的特色功能。不过，有时候也许你会发现，不小心多按了几下 u，你就退回到打开文件之前的版本去了。我想，这很有可能不是你想要的行为吧？如果你，像我一样，希望能够无限制地进行编辑撤销，同时还想在退回打开文件的状态之前能有一个提醒，那 undowarning.vim 可能就是你想要的。

这个插件不支持用包管理器自动安装。你需要自行下载 undowarning.vim，并把它放到你的 Vim 配置目录的 plugin 子目录下。下图是一个运行中的示例：

Undowarning 的效果

Rainbow

代码中括号多了，有时候眼睛就有点看不过来，需要有个更好的颜色提示。因此，就有了很多彩虹效果的 Vim 插件。在这些插件中，我最喜欢的是 frazrepo/vim-rainbow，它最妙的地方是，居然能把 C++ 代码中的尖括号也进行加亮，还能基本不会在出现小于、大于、流输入输出时进行错误的加亮。效果见下图：

彩虹括号的效果

效果默认不自动启用，可以用 :RainbowToggle 命令来切换，或用 :RainbowLoad 命令来加载。我觉得在括号多的时候按需启用挺好，推荐！

Auto-pairs

代码中永远有着大量成双成对的符号，输入一个，就自动出来另一个，会是一个非常有用的功能。但这样的功能，也需要处理一些特殊情况，比如，如果程序员输入了一对符号 ()，结果千万不能是 ())。在很多现代的编辑器上，这已经是个标准功能了，但 Vim 一直没有类似的功能。

实际上，Vim 里已经有插件 jiangmiao/auto-pairs 支持了这个功能，并解决了大部分边角情况。我觉得可以推荐给大家。

这个插件，要不要推荐我还是犹豫了一下的。我一开始对它相当满意，但后来我发现仍然有一些边角情况处理不好，使用它就会导致无法编辑出我需要的效果。所以我又把它卸载了。但再后来，我又觉得，毕竟瑕不掩瑜，而且有问题时把它禁用不就得了！

所以，它的配置项我也只需要提一个，就是禁用的键映射。这个键映射由全局变量 g:AutoPairsShortcutToggle 控制，默认值是 <M-p>。如果你在 Mac 上，这个键多半就不工作了，除非你只在 Mac 终端里使用 Vim，并且在终端应用里配置了“将 Option 键用作 Meta 键”。对于大部分 Mac 用户，你需要进行类似下面的配置（因为 Mac 上按 Option-P 会产生“π”）：

let g:AutoPairsShortcutToggle = 'π'

其他内容就请自行查看它的帮助文件了。

Largefile

如果你经常打开很大的日志文件，那 Vim 的一些自动功能可能不仅帮不了什么忙，反而会拖慢你的编辑速度。有一个 Vim 插件能在文件较大时自动关闭事件处理、撤销、语法加亮等功能，用来换取更快的处理速度和更短的响应时间。这个插件就是 vim-scripts/LargeFile。

这个插件的功能比较简单，唯一需要配置的就是多大算大。你只需要在 vimrc 配置文件中把你对大文件的阈值（以 MB 为单位）写到 g:LargeFile 变量里即可。比如，如果你认为超过 100MB 算大文件，那我们这样写就可以了：

let g:LargeFile = 100

Markdown Preview

你如果像我一样常常写 Markdown 的话，你应该会喜欢 Markdown Preview 这个插件。Markdown 本来最适用的场景就是浏览器，纯文本的 Vim 只能编辑，没有好的预览终究是很不足的。Markdown Preview 解决了这个问题，让你在编辑的同时，可以在浏览器里看到实际的渲染效果。更令我吃惊的是，这个预览是完全实时、同步的，无需存盘，而且预览页面随着光标在 Vim 里移动而跟着滚动，效果相当酷。你可以直接到 Markdown Preview 的主页上看一下官方的示意图，我就不在这里放动图了。

这个插件唯一需要特别注意的是，你不能直接把 iamcco/markdown-preview.nvim 放到你的包管理器里了事。原因是它里面包含了需要编译的前端组件，需要下载或编译才行。在它的主页上描述了在不同包管理器里的安装方式，你只要跟着照做就行。

它的配置在主页上也有列表，但默认设置就已经完全可用了。如果有需求的话，你可以修改其中部分值，如 g:mkdp_browser 可以用来设定你希望打开页面的浏览器（我目前设的是 'firefox'）。

Calendar

Calendar 是一个很简单的显示日历的 Vim 插件，在包管理器里的名字是 mattn/calendar-vim。它的功能应该就不需要解释了，效果可以直接查看下图。

:CalendarH 产生的水平日历

这个插件支持一些不同的样式和分割方式，上图中就是用 :CalendarH（或正常模式命令 caL）进行的横向分割，同时横向显示日历，你也可以用 :Calendar（或正常模式命令 cal）进行纵向分割和日历显示。此外，它还支持其他一些命令和组合。鉴于这个插件的的帮助文件也不长，如果你对相关功能有兴趣的话，就请你自己去看一下了。

Matrix

上面介绍的插件，不管对你有没有用，都可以说是“有用”的。插件也不一定要做有用的事，我的机器一直装着下面这个“没用”的插件，uguu-org/vim-matrix-screensaver。

它的效果不用解释，直接看下面的动图就好。

Matrix!

Killersheep

Vim 脚本不仅可以做没用的事情，还可以更进一步做娱乐的事情。比如，Vim 的作者 Bram 亲自操刀写了这个“愚蠢的游戏”，vim/killersheep。

这当然不是一个真正非常好玩的游戏，不过我也玩通关了。你不妨也试试？小提示：屏幕拉高点，按键重复速度快点，重复前延迟短一点，这样更有助于你打好这个游戏。

当然，这个插件的本来意义就不是一个游戏，而是要演示 Vim 8.2 的下列新功能：

带颜色和掩码的的弹出窗口

用于高亮文本的文本属性

声音

如果你想用这些功能的话，就可以去看看这个插件的源码。这也算一种寓教于乐吧？

技巧

行过滤

在编辑日志等类型的文本时，我们往往想过滤出我们感兴趣的内容。这时，我们可以用正则表达式，但使用 :s 命令并不是一种最高效的方式。如果你感兴趣的每一行都可以跟某个正则表达式模式匹配（如日期、某关键字等），最高效的命令应该是：

:v/匹配模式/d

稍微解释一下，:v 命令（可以查看帮助 :help :v）可以用来找出不符合匹配模式的行（对比一下 grep -v 命令），然后执行后面的动作。所以上面的命令就是找出不满足匹配模式的行，然后执行删除（d）。

顺便说一下，如果你需要找出符合匹配模式的行，需要的命令是 :g（可以查看帮助 :help :g）。

自动关闭最后一个 quickfix 窗口

我们已经讨论到很多功能会用到 quickfix 窗口。如果打开 quickfix 窗口后，你关闭了编辑的主窗口，那 quickfix 窗口可能就成为这个 Vim 会话里剩下的唯一窗口了，而这个窗口多半你完全不再需要。你会希望，此时 Vim 应该自动关闭这个窗口直接退出。这当然是个可以用程序自动化的事情，所以我们也应该这样做，用下面的脚本放到 vimrc 配置文件里就可以做到：

aug QFClose
  au!
  au WinEnter *  if winnr('$') == 1 && &buftype == "quickfix"|q|endif
aug END

你只要查一下 winnr 函数的帮助（:help winnr()）就很容易理解了，代码的意思还是非常清楚的：如果窗口的数量是 1 并且缓冲区类型是 quickfix 的话，那就退出 Vim。为了确保重复执行这段代码没有问题，它有一个自己的自动命令组，并会在清除这个自动命令组的所有自动命令后在进入窗口（WinEnter）这个事件中进行上面的检查。

（本技巧来自这个 Stack Overflow 回答。）

Home 键的行为

对于大部分现代的编辑器，Home 键的行为通常是：

当光标处于本行第一个非空白字符上时，跳转到行首

否则，跳转到本行第一个非空白字符上

虽然 Vim 的行为不是这样，但如果你希望配置出这样的行为，也不麻烦，把下面的代码加入到你的 vimrc 配置文件即可：

function! GoToFirstNonBlankOrFirstColumn()
  let cur_col = col('.')
  normal! ^
  if cur_col != 1 && cur_col == col('.')
    normal! 0
  endif
  return ''
endfunction

nnoremap <silent> <Home> :call GoToFirstNonBlankOrFirstColumn()<CR>
inoremap <silent> <Home> <C-R>=GoToFirstNonBlankOrFirstColumn()<CR>

在这个代码里，col('.') 用来获取光标所在的列号，然后我们跳转到第一个非空白字符处（^），随后检查是不是我们不在第 1 列并且列号没有变化。如果是的话，说明第一个非空白字符不在行首并且当前光标已经在第一个非空白字符处了，那我们就跳转到行首去（0）。

另外要注意，我们这儿使用了 normal! 而不是 normal。这两者的区别是，用了 ! 的 normal 命令会忽略键映射，否则 normal 就跟正常按键一样了。为了防止其他地方定义了键映射导致行为变化，一般推荐 normal! 命令而不是 normal 命令。

我们这样就修改了正常模式和插入模式下的 Home 键的行为。目前，在可视模式下这个方式不适用，你仍然只能手工选择合适的 0 或 ^ 命令。

查看光标下字符的内码

有很多字符是很相似的，在 Vim 使用的等宽字体中尤其如此。比如，光看字形，你能区分下面这些字符分别是什么吗？

- – — − ─

这些字符看起来虽然相似，但它们的意义是完全不同的。特别是在源代码中，如果你一不小心混入了一个相似的字符（字处理器有时候会自动替换一些 ASCII 字符，造成这种问题），代码运行就会出错了。这时，ga 命令就可以帮上忙。

下图展示了 ga 命令的一次执行结果：

执行 ga 命令的结果

我们可以看到这个字符实际上是 Unicode 字符 U+2013（十进制和八进制数值分别是 8211 和 20023），短破折号。这个字符在很多键盘上不能直接打出来，因而 Vim 提供了二合字母（digraph），可以用 <C-K>-N 的方式来输入。

为什么是 42？

好吧，最后这个不是技巧。你可能会奇怪：为什么在讲编程的很多代码里会出现 42 这个数字呢？那其实是因为一个科幻小说的“梗”。事实上，我在这个课程里也用到了 42。要想获取进一步的信息，请在 Vim 里输入：

:help 42

内容小结

本讲的内容比较零散，我为你介绍了一些我认为值得介绍的插件和技巧，每一则篇幅都较短，我也就不再正式总结了。

我分享的这些内容大部分是有用的，但也有一部分可以认为基本是“无用”的。生活里如果要求每一件事情都有用，岂不是会变得非常无趣？对我来说，编程和编程工具也是如此。

课后练习

今天介绍的插件和技巧，虽说不能算必需，也是非常有意义的。强烈建议你挑至少三个来安装 / 实验一下。如有任何问题或意见，欢迎留言和我交流。

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

结束语｜Vim 森林探秘，一切才刚刚开始

你好，我是吴咏炜。

我们的课程到这里就结束了，而你的学习旅程，到这儿只能算是一个小小的休息站。

学习的度

对于一个持续发展了 30 年的编辑器，我们显然不可能在一门短小精悍的课程里完整地覆盖它的所有功能。不过，我从来就没打算介绍 Vim 的一切。如果把 Vim 比作一片大森林，我只是一个导游，为你制定了一条旅游路线，带你绕过沼泽地和陷阱，攀上了几座峰顶，让你能够领略到若干美景。如果想在林中长久地居住下去，熟稔各条秘径，你仍然要靠自己去探索。

Vim 的作者 Bram 这么告诫人们不要走到两个极端上去 [Moolenaar 2007]：

你需要马上把文本准备好。所以没有时间读文档或学习新命令。——你会一直使用原始的命令。

你想学编辑器提供的所有功能，并在任何时候都能使用最高效的命令。——你会浪费很多时间学习很多你永远不会用到的东西。

前者的问题很明显，如果你不学习，那你只能使用初级的功能，所以效率一定很低。后者的问题可能不那么明显了：实际上，除了多花时间之外，你很难培养出良好的习惯，形成“肌肉记忆”。而这，恰恰是高效工作的关键之一——不需要想，就知道怎么做，从而可以把头脑和精力投入到更重要的问题上。

在这个课程里，我也只是告诉你基本的原则和技巧，并培养你基本的编辑习惯。回头，在遇到实际问题时，你会需要使用搜索引擎、讨论组等工具来找到问题的答案。

学习、积累和分享

我学习 Vim，原本是处于 Linux 下开发的需要，而后慢慢成为一种习惯。回过头来看，Vim 就和 Unix 一样，老而弥坚，经久不衰，不管时代怎样变化，它们却一直没有过时。而同时代我用过的其他 Windows 上的编辑器，如 EditPlus 和 UltraEdit，现在虽然都还在，但是应该已经很少有人提起了吧……

经过几年的学习和使用，我居然也就可以给别人分享我的经验了。我先是在 IBM developerWorks 上发了一系列的三篇 Vim 文章，后面又在 SHLUG（Shanghai Linux User Group）的活动中分享了我的 Vim 使用经验。事后，我在网上看到别人觉得我这个分享做得最好，也是非常的欣慰。

时间快进到今年的年初，我做完了我在极客时间的第一门课程《现代 C++ 实战 30 讲》。当编辑问我还有什么其他可分享的课程时，我立刻想到了 Vim。于是，就有了这个课程。我很喜欢知识分享的过程，因为准备的过程同时也是自我梳理和刷新的过程，毕竟要给别人讲，就不能像自己用的时候那样不求甚解了。让我没预料到的是，在课程中我还向某些积极的同学学了一两手（为了不让你们太骄傲，我就不点名了😉）。知识分享真是一种非常好的活动，于人于己都非常有利。建议大家在工作中也可以多多考虑😇。它唯一的缺点，就是会让自己变得非常忙碌、周末没有休息时间而已😂。

高效编辑的诀窍

现在回到 Vim。我们来考虑一下这个“元”问题：怎么样可以进行高效的编辑？

事实上，Bram 早就回答过这个问题了。这跟一般的效率改进计划没有本质的区别。我们要做的是：

发现低效的根源

找出更快的方法

形成新的习惯

Vim 的功能也是围绕着这样的模式开发出来的。

比如，我们要找出当前文件中某个符号的使用，在任何编辑器里，都会提供一个搜索的功能。但每次都使用搜索，实际上也是有点麻烦的。Vim 里的 * 搜索键和搜索加亮，这两个你目前应当已经习以为常的功能，就是为了解决这种低效而诞生的。而对你，现在更快的方法，就是在配置中启用搜索加亮（目前我们的基本配置里已经启用），并使用 * 来搜索光标下的单词。

又比如，我们有一个非常长的函数名，打起来又费力又容易出错。这时候，我们需要找出更快的方法，那就是自动完成。Vim 内置的改进方法是 <C-N> 和 <C-P> 命令。而我们学到现在的就该知道，YCM 还提供了更现代的模糊完成引擎。用好内置命令，或安装合适的插件，就是我们需要形成的新习惯。

说到这儿，你也看到了，不仅我们的课程不是高效编辑的终点，而且就连 Vim 也不是。Vim 一直在发展，但有人嫌 Vim 发展得不够激进，另外搞了 Neovim。我们这个课程完全没有讨论 Neovim，主要是不想在一个已经很复杂的课题上再增加复杂性（同时也是因为 Neovim 虽然看起来势头不错，但远没到可以尘埃落定、一定会在将来替代 Vim 的程度，再加上两者并不完全兼容……反过来，Neovim 倒是刺激 Vim 更快地添加新功能了）。

抛开 Neovim 不谈，我们还有插件：正是这些辛勤的插件作者，才使得 Vim 真正更为强大，成为一个特别高效的编辑器。就像上面讨论的，有了 YCM，我们找到了一个比 Vim 内置功能更加简单、直观、高效的方法。我们也应该去用好这样的新工具，提升自己的工作效率。

要提高自己的编辑效率，需要时时刻刻注意自己有哪儿效率特别低，出现了不必要的重复，然后找到更好的办法来改进，并确保自己形成新的习惯。

这个新的习惯是什么呢？可以是掌握了 Vim 里的一个之前不熟悉的功能，可以是用上了一个新的插件，也可以是……为 Vim 社区贡献一个新的插件！

这也恰恰是我在开篇词里提到的“懒惰”。所谓懒惰，就是我们要不让自己做低效的重复工作。而要不做低效的重复工作，我们就需要开动自己的脑子，监测自己的工作方式，找出问题点，予以改进，并坚持下去。懒惰的手段就是高效；反过来说也可以，高效的目的就是懒惰。

期待你在 Vim 森林里找出自己的“传送门”，能够慵懒地一抬腿，就飞速到达任何自己想去的地方。

我们后会有期！

《Vim 实用技巧必知必会》课程结束了，这里有一份毕业问卷，题目不多，希望你能花两分钟填一下。十分期待能听到你说一说，你对这个课程的想法和建议。

参考资料

作为写作的基本规矩，我最后列一下我参考过的资料（插件和之前文中直接给出的链接则不再重复）。希望它们也能帮到你，让你在下面的旅途中再上一层楼：

Allen, Leo. 2016. “Why Vim is so much better than Atom”.

Arthur, Barry. 2014. “Learning the tool of Vim”.

Bringhurst, Robert. 2012. The Elements of Typographic Style, 4th ed. Hartley & Marks Publishers.

Irwin, Conrad. 2013. “Bracketed paste mode”.

Kochkov, Anton. 2019. “Terminal Colors”.

Leonard, Andrew. 2000. “BSD Unix: Power to the people, from the code”.

Moolenaar, Bram. 2000. “The continuing history of Vim”.

Moolenaar, Bram. 2002. “Vim, an open-source text editor”.

Moolenaar, Bram. 2007. “7 habits for effective text editing 2.0”. YouTube video.

Moolenaar, Bram. 2018. “Vim: Recent developments”.

Neil, Drew. 2015. Practical Vim, 2nd ed. O’Reilly. 中文版：杨源、车文隆译《Vim 实用技巧》，人民邮电出版社，2014（第一版），2016（第二版）。

Neil, Drew. 2018. Modern Vim: Craft your development Environment with Vim 8 and Neovim. Pragmatic Bookshelf. 中文版：死月译《精通 Vim：用 Vim 8 和 Neovim 实现高效开发》，电子工业出版社，2020。

Ornbo, George. 2019. “Vim: you don’t need NERDtree or (maybe) netrw”.

Osipov, Ruslan. 2018. Mastering Vim. Packt. 中文版：王文涛译《Vim 8 文本处理实战》，人民邮电出版社，2020。

Robbins, Arnold, Elbert Hannah, and Linda Lamb. 2008. Learning the vi and Vim Editors, 7th ed. O’Reilly.

Salus, Peter H. 1994. A Quarter Century of UNIX. Addison-Wesley.

Schneider, Peter A. 2018. Answer to “How do I disable the weird characters from ‘bracketed paste mode’ on the Mac OS X default terminal?”.

Stack Overflow. 2015. “2015 developer survey”, section “Technology > Text editor”.

Stack Overflow. 2019. “Developer survey results 2019”, section “Technology > Development environments and tools”.

Target, Sinclair. 2018. “Where Vim came from”.

Vance, Ashlee. 2003. “Bill Joy’s greatest gift to man – the vi editor”.

Vim Online.

Wikipedia. “Berkeley Software Distribution”.

Wikipedia. “Bill Joy”.

Wikipedia. “ex (text editor)”.

Wikipedia. “Version 6 Unix”.

Wikipedia. “vi”.

Wikipedia. “Vim”.

Wikipedia. “Visual editor”.