mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 14:43:42 +08:00
del
This commit is contained in:
179
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/11|文本的细节:关于字符、编码、行你所需要知道的一切.md
Normal file
179
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/11|文本的细节:关于字符、编码、行你所需要知道的一切.md
Normal file
@@ -0,0 +1,179 @@
|
||||
<audio id="audio" title="11|文本的细节:关于字符、编码、行你所需要知道的一切" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/62/f9/62yya48045079107595bd90bc8fe17f9.mp3"></audio>
|
||||
|
||||
你好,我是吴咏炜。
|
||||
|
||||
从今天开始,我们进入提高篇和拓展篇的学习。
|
||||
|
||||
在提高篇,我会带你对 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,美国信息交换标准代码)兼容,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cc/35/cc7fb695569c7ea460c1b89fc7859735.gif" alt="Fig11.1" title="ASCII 字符映射表">
|
||||
|
||||
注意,头 32 个字符和最后一个字符是控制字符,其中大部分现在已经很少有人使用了,但还有一些我们今天仍然会在不同的场合遇到,如马上就会讨论的 LF 和 CR。
|
||||
|
||||
ASCII 是美国标准,里面只有基本的拉丁字母,对其他国家来讲可能就不合适。比如对欧洲国家来说,ASCII 既没有带变音符的拉丁字母(如 é 和 ä ),也不支持像希腊字母(如 α、β、γ)、西里尔字母(如 Пушкин)这样的其他欧洲文字,使用起来很不方便。很多其他编码方式使用了 128-255 的字节值范围作为扩展,总共最多是 256 个字符,一次允许一套方式生效,称之为一个代码页(code page)。这种做法,只能适用于文字相近、且字符数不多的国家。比如,下图表示的 ISO-8859-1(也称作 Latin-1)和后面的 Windows 扩展代码页 1252(下图中绿框部分为 Windows 的扩展),就只能适用于西欧国家。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/95/ab06af7037bd09d229efbb693be42195.png" alt="Fig11.2" title="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 绘制):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/0f/da18e20f4a929399d63a467760657c0f.png" alt="Fig11.3" title="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`)的原因。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/6f/ca3ce8c43f98cf60bb3c73bd4c2b3d6f.png" alt="Fig11.4" title="部分汉字的中文和日文字形对比">
|
||||
|
||||
跟中文字符集中“半角”和“全角”的概念有点像,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` 才比较有意义:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/65/d8ec7eef11d42da4a56ac1d70bf45c65.png" alt="Fig11.5" title="设置了 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` 等命令一样,可以先在可视模式下选定文本,也可以在命令之后跟动作键。对于源代码,它的妙处在于它知道什么是注释,什么是列表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c8/59/c8877e7b766d03f685b34ef1bbf05759.gif" alt="Fig11.6" title="设置行宽为 64、使用数字列表,然后格式化注释">
|
||||
|
||||
如果对这些功能有兴趣的话,请查看相关的帮助:[`:help gq`](https://yianwillis.github.io/vimcdoc/doc/change.html#gq) 和 [`:help fo-table`](https://yianwillis.github.io/vimcdoc/doc/change.html#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”后的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bc/eb/bcb413d29a169ddc766f7cc74f3a63eb.png" alt="Fig11.7" title="用二进制模式打开一个 PNG 文件">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b8/79/b88f5d5b3650c672deaf7129cff20979.png" alt="Fig11.8" title="转换成 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](https://github.com/mbbill/fencview)(注意,在 Windows 下你现在也不需要下载 iconv.dll 了:Vim 8 的 Windows 安装包中现在已经包含了 libiconv-2.dll)。
|
||||
|
||||
如果你使用的是 Linux 或 macOS 的话,可以键入 `iconv -l` 来看一下 libiconv 支持的编码方式:Vim 内部就是使用 libiconv 来实现编码转换的。
|
||||
|
||||
过去我们会有一些文件用后缀表示文件编码,比如 .gb 是 gbk 编码,.big5 是 big5 编码,.nfo 是 cp437 编码,你能想出如何正确载入这些文件的方法吗?
|
||||
|
||||
今天就到这里了。你的回答或问题,都可以留言告诉我。我们下一讲再见!
|
||||
219
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/12|语法加亮和配色方案:颜即正义.md
Normal file
219
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/12|语法加亮和配色方案:颜即正义.md
Normal file
@@ -0,0 +1,219 @@
|
||||
<audio id="audio" title="12|语法加亮和配色方案:颜即正义" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/27/ac/274712806c2ee5c77d7bc51afe8bc5ac.mp3"></audio>
|
||||
|
||||
你好,我是吴咏炜。
|
||||
|
||||
语法加亮这个功能,我们都非常熟悉。和 vi 刚出现的时代不同,它现在已经成为编程的基本功能了。在我们使用的各种代码编辑器中,都有语法加亮的功能。我们甚至可以拿一句俗语反过来说:没见过猪跑,还能没吃过猪肉么?
|
||||
|
||||
但是,你有没有想过,语法加亮到底是怎么实现的呢?今天,我们就不仅要尝尝不同“风味”的猪肉,还要进一步看看猪到底是怎么跑的——这样,我们才能选择,然后调整出,最符合自己口味的大菜。
|
||||
|
||||
## 语法加亮
|
||||
|
||||
在[第 8 讲](https://time.geekbang.org/column/article/271208)里,我们已经提到,Vim 的语法加亮依靠的是在 syntax 目录下的运行支持文件。今天,我就通过例子给你解说一下,Vim 里如何实现语法加亮,然后语法加亮又如何映射到屏幕上的颜色和字体。
|
||||
|
||||
我们先来看一个比较简单的例子,xxd。
|
||||
|
||||
xxd 这个名字看起来,是不是有点陌生又有点熟悉?其实,我们在第 11 讲还刚讲过 xxd:它是一个把二进制文件转换成地址加十六进制数值再加可读 ASCII 文本的工具,它的输出格式在 Vim 里也被称作 xxd。不过,在用菜单项或 `:%!xxd` 命令转换之后,Vim 并不会自动使用 xxd 格式。要应用 xxd 格式的语法加亮,我们需要使用自动命令(可以参考 [`:help using-xxd`](https://yianwillis.github.io/vimcdoc/doc/tips.html#using-xxd)),或者手工使用命令 `:setf xxd`。下图是对上次的二进制文件使用了 xxd 语法加亮的效果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/25/9c4d080fd180c560757305d4a1645225.png" alt="Fig13.1" title="使用了 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”)的关系。
|
||||
|
||||
1. **xxdAddress。**它是地址匹配,所以匹配条件是从行首开始的一个或更多的十六进制字符后面跟一个冒号。
|
||||
1. **xxdSep。**它是分隔符,仅匹配 xxdAddress 中的冒号部分,也算是地址的一部分。
|
||||
1. **xxdAscii。**它是右边的 ASCII 字符部分,条件是两个空格后面跟最多 16 个字符,然后是可选的 CR 字符(`\=` 和 `\?` 效果相同),然后必须是一行结束。
|
||||
1. **xxdDot。**它是对“.”和 CR 字符的特殊匹配,可以留意一下上面图里“.”和其他字符的加亮效果的不同之处。同样,这个句点也属于 ASCII 字符部分。
|
||||
|
||||
上面的正则表达式都比较简单,唯一之前没出现过的是第 3 个正则表达式后面的 `hs=s+2`:它的含义是语法加亮的起始位置是模式匹配部分的开始位置再加 2(可查看 [`:help :syn-pattern-offset`](https://yianwillis.github.io/vimcdoc/doc/syntax.html#: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`](https://yianwillis.github.io/vimcdoc/doc/syntax.html#: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,亮黄色。
|
||||
|
||||
使用这个配色方案在图形界面和色彩终端下的效果,如下面的截图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/83/3b/832e5d45ca49423208yy20742458fc3b.png" alt="Fig13.2" title="图形界面下 koehler 配色方案的效果">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/86/c6/86402e7b171a00dac3b35945e89c4bc6.png" alt="Fig13.3" title="色彩终端下 koehler 配色方案的效果">
|
||||
|
||||
### 配色方案在终端下的优化
|
||||
|
||||
说到这里,我们有必要来讨论一下 Vim 里允许使用的色彩数量。在图形界面 Vim 里,色彩是 Vim 本身调用系统的编程接口来控制的,可以使用 RGB 的所有 16,777,216 种不同颜色。但在终端里,Vim 会受到终端能力的限制,只能根据终端的能力来显示色彩。根据终端的类型,我们可以分为 4 种情况:
|
||||
|
||||
第 1 种是最古老的是单色终端,没有颜色,只能使用下划线、粗体等效果。效果定义使用 `term=…` 的形式。今天,我们应该基本碰不到这样的环境了。
|
||||
|
||||
第 2 种是 8/16 色终端,允许使用最基本的八种颜色(黑、红、绿、黄、蓝、紫、青、白),以及这些颜色的较亮变体(即使 8 色终端一般也能在前景色上使用加亮的变体)。我们可以使用 `cterm=…` 定义粗体等效果(由于兼容性问题,不常用),用 `ctermfg=…` 和 `ctermbg=…` 定义前景和背景色,其中可以使用英文色彩名称或序号(见 [`:help cterm-colors`](https://yianwillis.github.io/vimcdoc/doc/syntax.html#cterm-colors))。鉴于序号在不同的环境里可能是不同的,我们一般使用色彩名称。如果你使用非图形界面终端,可能会遇到这种情况,但这应当也很不常见了吧。
|
||||
|
||||
这些颜色虽然是标准的,但很多终端允许用户调整这些颜色,以达到最好的色彩组合效果。比如,下图是 macOS 里终端应用的一个设置界面,其中的“ANSI 颜色”就是用户可以调整的 16 种“标准色”:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a9/ae/a9ea06f1895yy47154a5630970ae2aae.png" alt="Fig13.4" title="macOS 终端应用的文本设置界面">
|
||||
|
||||
第 3 种是 256 色终端,用户可以选择预先定义的 256 种颜色之一,这在目前的终端里是非常主流的方式了。你可以在网上很方便地找到[脚本](https://github.com/eikenb/terminal-colors)来输出这些颜色,效果如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/cb/c70b3875f2f3dc563401d555152867cb.png" alt="Fig13.5" title="在 iTerm2 下使用脚本输出的 256 种颜色">
|
||||
|
||||
要选择这 256 种颜色中的一种,方式不太直观:你需要使用 `ctermfg=…` 和 `ctermbg=…`,并直接写出这 256 种颜色之一的编号。
|
||||
|
||||
这 256 种颜色都可以算是标准的,它们的标准 RGB 值有明确的定义。头 16 种颜色就是上面的“ANSI 颜色”,在终端里常常可以直接调整,图中也可以看到和前面图里的颜色已经有明显的不同。虽然界面只提供了头 16 种颜色的调整,但为了达到最佳的显示效果,你也可以编程修改这 256 种颜色的调色板。
|
||||
|
||||
第 4 种是支持真彩(truecolor)的终端,跟编程修改 256 色的调色板相比,这是更简单的做法。下面是部分比较常见的支持 RGB 真彩的终端([此处](https://gist.github.com/XVilka/8346728)是一个更完整的列表):
|
||||
|
||||
- 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](https://github.com/morhetz/gruvbox)(包管理器中的安装名称是“morhetz/gruvbox”)。它不仅是一个支持深色背景和浅色背景的配色方案,而且还特意确保自己能和 Vim 的一些最流行插件兼容。下面是这个配色方案的示意截图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/ed/e9c9cb2c6ff4bf1e4efe2c11352df1ed.png" alt="Fig13.6" title="浅色背景下的 gruvbox 效果图">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/7f/6915526e3e28877a8ecf58600ffdc77f.png" alt="Fig13.7" title="深色背景下的 gruvbox 效果图">
|
||||
|
||||
我觉得 gruvbox 在深色背景下还是挺漂亮的。它的颜色总体偏暖,而你如果喜欢深色背景下较为清冷的色彩,我觉得 [jellybeans](https://github.com/nanotech/jellybeans.vim)(包管理器中的安装名称是“nanotech/jellybeans.vim”)还不错:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e1/22/e1ebd6a01641eeedbaa8d7d6890d2d22.png" alt="Fig13.8" title="使用深色背景的 jellybeans 效果图">
|
||||
|
||||
不过,这两种方案我都是偶尔使用,我使用更多的还是明白(mbbill)设计的 [desertEx](https://github.com/mbbill/desertEx)(包管理器中的安装名称是“mbbill/desertEx”)的一个版本。它的特点是无加亮的普通文字对比度设得比较低,读起来比较轻松(但加亮部分效果仍然比较强烈)。如果你想试试这个方案的话,可以自己安装尝试一下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6e/6c/6e0aa1889cba9e8e17fd50224d0c276c.png" alt="Fig13.9" title="我用的 desertEx 效果图(非最新版本)">
|
||||
|
||||
### 检查/调试配色方案
|
||||
|
||||
如果你想自己对配色方案进行调整的话,有一个小工具肯定会非常有用,那就是 vim-scripts/SyntaxAttr.vim。不过,这个插件不会自己添加键映射,需要你在用包管理器安装之后,自己在 vimrc 配置文件中加入类似下面的语句:
|
||||
|
||||
```
|
||||
nnoremap <Leader>a :call SyntaxAttr()<CR>
|
||||
|
||||
```
|
||||
|
||||
这样,我们就能用 `\a` 来检查光标下面的语法高亮详情了。下面是一个示例:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/01/6bf1c5b8a5d8c43e68540d29944c5a01.png" alt="Fig13.10" title="检查光标下文本的加亮组">
|
||||
|
||||
从上面可以看到,`constexpr` 属于 cppStorageClass 语法加亮组(这是在 syntax/cpp.vim 中定义的),并且被链接到了 Type 加亮组。后面的 guifg 和 gui 设定就是 Type 加亮组的内容:使用色彩 tan1(RGB 值为 #ffa54f),特殊效果为粗体(bold)。
|
||||
|
||||
在你自己设计、调试语法文件或配色方案时,你会发现这个工具非常有用。
|
||||
|
||||
## 输出加亮效果
|
||||
|
||||
作为一个文本编辑器,Vim 只接受文本的复制和粘贴。如果你想要在一个(非 Markdown)文档中展示有语法加亮的代码,Vim 也是可以用来产生这样的代码的——通过 HTML 输出。
|
||||
|
||||
Vim 默认就提供了 `:TOhtml` 命令(参见[第 4 讲](https://time.geekbang.org/column/article/267765)中讨论的系统内置插件),可以把当前展示的语法加亮效果输出为一个 HTML 文件。你可以根据最终文档的要求,选择合适的深色或浅色配色方案,然后使用该命令来输出 HTML。这个命令默认输出整个文件,你也可以自己在可视模式下选定范围,或者用逗号隔开的行号选定范围,这样 `:TOhtml` 命令就只会输出选定范围的 HTML 代码。
|
||||
|
||||
这个方式灵活是挺灵活,但不能直接把带色彩加亮的文本贴到文档里去,终究还是不太方便。令人欣慰的是,早就有人找到了在各个平台上把 HTML 代码转成剪贴板富文本的方法。我最近对一个 Vim 插件 [vim-copy-as-rtf](https://github.com/adah1972/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 里面的颜色定义,了解它是如何解决在不同终端下面的颜色一致性问题的。
|
||||
|
||||
如果有任何问题,欢迎留言和我交流。
|
||||
|
||||
我是吴咏炜,我们下一讲再见!
|
||||
318
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/13|YouCompleteMe:Vim 里的自动完成.md
Normal file
318
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/13|YouCompleteMe:Vim 里的自动完成.md
Normal file
@@ -0,0 +1,318 @@
|
||||
<audio id="audio" title="13|YouCompleteMe:Vim 里的自动完成" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f2/d7/f241b1d11fbcddf1a6dd7cb5214dc4d7.mp3"></audio>
|
||||
|
||||
你好,我是吴咏炜。
|
||||
|
||||
在集成开发环境里,自动完成是一个非常重要的功能。可是 Vim 并不能真正理解你输入的代码,因此它自身无法提供自动完成的功能。不过,Vim 仍然提供了一些接口,允许第三方的软件实现这样的功能,并和 Vim 自身进行集成。[YouCompleteMe](https://github.com/ycm-core/YouCompleteMe)(简称YCM)就是这样的一个第三方软件,今天,我就为你详细介绍一下它。
|
||||
|
||||
YCM 对 C++ 程序员最为适合,它可以提供其他工具实现不了的功能。而且,它也适用于很多其他语言,包括 C 家族的各种语言和其他常用的语言,如 Python、Java 和 Go 等。即使在 YCM 不直接支持你使用的语言的时候,它仍然能通过标识符完成功能提供比没有 YCM(和其他语言支持插件)时更好的编辑体验。因此,我推荐你使用这个插件。
|
||||
|
||||
## YouCompleteMe
|
||||
|
||||
### 功能简介
|
||||
|
||||
首先我来介绍一下 YCM 的基本功能吧。根据它的主页(我的翻译):
|
||||
|
||||
>
|
||||
<p>YouCompleteMe 是一个快速、即输即查、模糊搜索的 Vim 代码完成引擎。它实际上有好几个完成引擎:<br>
|
||||
</p>
|
||||
<ul>
|
||||
- 一个基于标识符的引擎,可以在任何编程语言中工作
|
||||
- 一个强大的基于 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)系统提供的数据来为很多其他语言提供语义完成
|
||||
</ul>
|
||||
|
||||
|
||||
其实,Vim 里的自动完成插件并不止这一个,但 YCM 是比较成熟也比较全面的。虽说它的安装配置有一定的复杂性,但比起另外一些要求你独立安装、配置语言服务器的方案,它至少能一次性搞定插件和你需要的语言支持,所以反而算是简单的了。我最近的主要开发语言是 C、C++、Python 和 Vim 脚本,因此这也算是个很完美的匹配了。
|
||||
|
||||
下面是一个简单的示例,展示了 YCM 的效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ee/a9/ee16421c2d6e17108e60d43c2a174ea9.gif" alt="Fig13.1" title="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 的包管理器来安装,那样会出什么错都搞不清楚。大致安装过程是:
|
||||
|
||||
1. 选择安装目录
|
||||
1. 签出 YCM
|
||||
1. 根据你需要使用的语言使用合适的选项,来进行编译安装
|
||||
|
||||
下面,我们就快速地过一下。
|
||||
|
||||
首先,我们需要给 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 引擎的话,效果是这样的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0b/8c/0ba17b6b2231f4fd76315c5dac3ecd8c.gif" alt="Fig13.2" title="使用 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 的文件。找到之后,它就会弹出一个提醒,提示用户是不是要载入这个文件,运行其中的代码来得到需要的配置信息。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/88/47/889f835a5cb12359250b44f70e06d547.png" alt="Fig13.3" title="YCM 的配置载入提示">
|
||||
|
||||
虽然这个恼人的提示可以关掉,但这实际上会带来潜在的安全问题,毕竟 YCM 是会运行其中的 Python 代码的。此外,写这个文件也算是件麻烦事吧,尤其对不熟悉 Python 的人而言。
|
||||
|
||||
在 2014—2015 年期间,有人维护了一个叫 ycmconf 的插件,以一种我喜欢的方式解决了这个问题。不过,今天再直接用这个插件,就有点问题了。所以我在 GitHub 上复刻了这个插件,并进行了更新。如果你使用 YCM 进行 C 系语言的自动完成,那我推荐你安装 adah1972/ycmconf 这个插件。
|
||||
|
||||
这个插件支持两种简单的方式来配置 C/C++ 的自动识别:
|
||||
|
||||
1. 使用 CMake 输出的 compile_commands.json 文件(由于 YCM 目前已经直接支持该文件,这只对较老版本的 YCM 有意义)
|
||||
1. 使用上一讲(拓展 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>
|
||||
|
||||
```
|
||||
|
||||
自动修正功能可以参考下图的演示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/a3/1cc7ca1e72aea567b971f878eb1b98a3.gif" alt="Fig13.4" title="YCM 自动修正功能的演示">
|
||||
|
||||
自动修正的范围很广,小到修正一个拼写错误或者漏写名空间这样的问题,大到提供安全方面或代码风格现代化的调整(它可以利用你的 [Clang-Tidy](https://clang.llvm.org/extra/clang-tidy/) 配置来向你报告错误和提供修正建议)。有了它,编码工作真的轻松了许多。
|
||||
|
||||
我们这儿有四种跳转:
|
||||
|
||||
1. `GoTo`,无脑跳转,最常用的就是这个功能,如果能跳转到定义,就跳转到定义,否则就跳转到声明。
|
||||
1. `GoToDefinition`,顾名思义,就是跳转到定义。
|
||||
1. `GoToDeclaration`,专门跳转到声明。
|
||||
1. `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` 用来禁用光标在一个符号上长期停留出现的自动文档提示。未禁用时的效果如下图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/53/96/53a1e8f6886ac25a751bfb33c2942896.png" alt="Fig13.5" title="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](https://github.com/Andersbakken/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)即可。下面是一个示例:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3f/9b/3f67961bc0bf2c08d8f50ed5b090af9b.gif" alt="Fig13.6" title="Vim-rtags 的使用示例">
|
||||
|
||||
这个结果出来的速度比用 `:grep` 可快多了!
|
||||
|
||||
它还有很多其他命令,可以用来查找定义、**查找父类**、**查找子类**、**显示调用树**,等等。有些功能在 YCM 里并没有对应物,这也是 RTags 的价值了。你可以在 [vim-rtags 的主页](https://github.com/lyuts/vim-rtags)上查看学习所有这些命令及其快捷键。
|
||||
|
||||
## 内容小结
|
||||
|
||||
在本讲中,我们主要介绍了 YouCompleteMe 这个重量级插件,包括其安装和配置。我们可以看到,在插件的帮助下,我们可以获得不输于集成开发环境的自动完成体验,同时,仍然享受 Vim 的快速启动和强大编辑功能。最后我们花了一点点时间介绍了 RTags 工具和 vim-rtags 插件,它在你写 C 族语言而不能使用 clangd 引擎时会特别有帮助。
|
||||
|
||||
YCM 的参数和键映射我写到了示例配置文件里,对应的标签是 `l13-unix` 和 `l13-windows`。
|
||||
|
||||
## 课后练习
|
||||
|
||||
今天学完之后的主要任务,当然就是把 YouCompleteMe 装起来、配置好了。它的使用反而是相当简单的,大部分情况下使用 `<Tab>` 就行。
|
||||
|
||||
至于 RTags,Unix 下的 C++ 程序员们可以根据自己的兴趣,决定是否捣腾一下。这个工具还是有点可玩性的。
|
||||
|
||||
如果遇到什么问题,欢迎留言和我讨论。我们下一讲再见!
|
||||
329
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/14|Vim 脚本简介:开始你的深度定制.md
Normal file
329
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/14|Vim 脚本简介:开始你的深度定制.md
Normal file
@@ -0,0 +1,329 @@
|
||||
<audio id="audio" title="14|Vim 脚本简介:开始你的深度定制" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/51/d3/517e393f6936689f28584d02dc0773d3.mp3"></audio>
|
||||
|
||||
你好,我是吴咏炜。
|
||||
|
||||
学到今天,我们已经看到了很多的 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`](https://yianwillis.github.io/vimcdoc/doc/eval.html#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`](https://yianwillis.github.io/vimcdoc/doc/map.html#user-commands))。注意这个命令的定义要写在一行里,所以如果命令很长,或者中间出现会吞掉 `|` 的命令的话,我们就会需要用上 `execute` 命令了。
|
||||
|
||||
最后,我再说明一下我们用过的 `map` 系列键映射命令(详见 [`:help key-mapping`](https://yianwillis.github.io/vimcdoc/doc/map.html#key-mapping))。这些命令的主干是 `map`,然后前面可以插入 `nore` 表示键映射的结果不再重新映射,最前面用 `n`、`v`、`i` 等字母表示适用的 Vim 模式。在绝大部分情况下,我们都会使用带 `nore` 这种方式,表示结果不再进行映射(排除偶尔偷懒的情况)。但是,如果我们的 `map` 命令的右侧用到了已有的(如某个插件带来的)键映射,我们就必须使用没有 `nore` 的版本了。
|
||||
|
||||
### 事件
|
||||
|
||||
和用户主动发起的命令相对应,Vim 里的自动处理依赖于 Vim 里的事件。迄今为止,我们已经遇到了下面这些事件:
|
||||
|
||||
- BufNewFile 事件在创建一个新文件时触发
|
||||
- BufRead(跟 BufReadPost 相同)事件在读入一个文件后触发
|
||||
- BufWritePost 事件在把整个缓冲区写回到文件之后触发
|
||||
- FileType 事件在设置文件类型(`filetype` 选项)时被触发
|
||||
|
||||
Vim 里的事件还有很多(详见 [`:help autocmd-events-abc`](https://yianwillis.github.io/vimcdoc/doc/autocmd.html#autocmd-events-abc)),我们就不一一介绍了。上面这些是我们最常用的,你应该了解它们的意义。
|
||||
|
||||
### 内置函数
|
||||
|
||||
Vim 里内置了很多函数(列表见 [`:help function-list`](https://yianwillis.github.io/vimcdoc/doc/usr_41.html#function-list)),可以实现编程语言所需要的基本功能。我们目前用得比较多的是下面这两个:
|
||||
|
||||
- `exists` 用来检测某一符号(变量、函数等)是否已经存在。在 Vim 脚本里最常见的用途是检测某一变量是否已经被定义。
|
||||
- `has` 用来检测某一 Vim 特性(列表见 [`:help feature-list`](https://yianwillis.github.io/vimcdoc/doc/eval.html#feature-list))是否存在。帮助文档里已经描述得很清楚,我就不详细介绍了。你可以对照看一下我们的 vimrc 配置文件里的用法,应该就明白了。
|
||||
|
||||
Vim 的内置函数真的很多,我也没法一一介绍。你可以稍作浏览,了解其大概,然后在使用中根据需要查询。别忘了,在看 Vim 脚本时,在关键字上按下 `K` 就可以查看这个关键字的帮助,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/34/30/3477651fbc0c79f285e9612180cfc030.gif" alt="Fig14.1" title="在 Vim 脚本里使用 K 键查看帮助">
|
||||
|
||||
## 风格指南
|
||||
|
||||
结束 Vim 脚本的介绍之前,我向你推荐一下 Google 出品的 Vim 脚本风格指南,[Google Vimscript Style Guide](https://google.github.io/styleguide/vimscriptguide.xml)。写一种语言,有一个风格指南肯定是会有帮助的,尤其对于初学者而言。
|
||||
|
||||
## 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](https://time.geekbang.org/column/article/278870) 里给出了一段代码,用 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`](https://yianwillis.github.io/vimcdoc/doc/if_pyth.html#python-current)),包括行、缓冲区、窗口等
|
||||
|
||||
此外,在拓展 2 里我们给出的使用 `pyxf` 来执行一个 Python 脚本文件,也是一种在 Vim 里调用 Python 的方式(详见 [`:help pyxfile`](https://yianwillis.github.io/vimcdoc/doc/if_pyth.html#: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` 直接跳转到光标下的文件里。
|
||||
|
||||
如果遇到什么问题,欢迎留言和我讨论。我们下一讲再见!
|
||||
259
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/15|插件荟萃:不可或缺的插件.md
Normal file
259
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/15|插件荟萃:不可或缺的插件.md
Normal file
@@ -0,0 +1,259 @@
|
||||
<audio id="audio" title="15|插件荟萃:不可或缺的插件" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/39/85/39ac9d79775714c6f2007bd090223c85.mp3"></audio>
|
||||
|
||||
你好,我是吴咏炜。
|
||||
|
||||
关于 Vim 的基本知识,我们已经讨论得差不多了。下面,我们需要的是练习和积累,在实践中成长。在今天的这一讲里,我们就来看看一些我们之前还没来得及介绍、但真的“必需”的插件,让你的开发效率再进行一次大幅提升。
|
||||
|
||||
跟之前各讲比起来,这讲会比较轻松。不过,在你已经学过了 Vim 的基本原理之后(特别是如果你学习了[拓展 4](https://time.geekbang.org/column/article/280731) 的样例,进一步了解了插件代码是如何编写的话),我希望你看到这些插件时,不仅可以看到它们的外观和用法,而且还能大概知道它们的工作原理。这样,你就不再仅仅是一个初级用户,而是已经晋升为真正的 Vim 高手,能够根据自己的需要进行定制,甚至是“魔改”了。
|
||||
|
||||
## Fugitive
|
||||
|
||||
对于大部分开发者来说,使用 Git 应该已经和呼吸空气一样自然了吧。我跟很多工作经历丰富的开发者一样,从 CVS 的年代开始(那时我还一直维护着 [CVSMenu](https://www.vim.org/scripts/script.php?script_id=1245)),经历了 SVN,然后看着 Git 慢慢一统天下,号令江湖。在 Vim 中如何高效地使用 Git,当然也就成了个不得不讨论的话题。
|
||||
|
||||
今天,我会介绍两个 Git 插件。一个是重量级的、功能很多的 Fugitive,一个是集中在几个特色小功能上的 GitGutter。下面我们就从 Fugitive 开始。
|
||||
|
||||
### 安装和配置
|
||||
|
||||
[Fugitive](https://github.com/tpope/vim-fugitive) 是纯 Vim 脚本,我们使用包管理器安装 tpope/vim-fugitive 即可(对了,他也是 Time Pope 大牛写的)。
|
||||
|
||||
这个插件基本上不需要配置,即装即用。
|
||||
|
||||
### 使用
|
||||
|
||||
Fugitive 是一个全功能的 Git 支持插件,支持在 Vim 里直接使用 Git 命令:你只需要把“G”变成大写即可。比如,原本你要在 shell 使用 `git log`,现在在 Vim 里使用 `:Git log` 命令即可。结果会在一个 Vim 窗口里打开,并且有着语法加亮的效果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/87/1d33862fae5ce87d891116770f254587.png" alt="Fig15.1" title=":Git log 命令的效果">
|
||||
|
||||
Fugitive 有很多命令,你可以在它的帮助文件里找到较为完整的信息。下面我从使用的角度说明一下最常用的命令,重点是对当前文件操作的命令。像 `:Git pull` 这样的命令,我就略过不讲啦。
|
||||
|
||||
首先,要把当前文件的修改存盘并加到 Git 的暂存区,可以使用 `:Gwrite`。
|
||||
|
||||
反过来,要放弃当前的修改,回到暂存区的状态,可以使用 `:Gread`。
|
||||
|
||||
如果你需要对当前文件进行移动、更名或删除,可以分别使用 `:GMove`、`:GRename`、`:GDelete`。跟命令行上的 git 命令不同,你不需要给出当前文件的名字。
|
||||
|
||||
要在 quickfix 窗口里查看当前文件的修改历史,可以使用 `:0Gclog`(`Gclog` 是查看整个项目的历史,对大项目使用 quickfix 窗口可能会有性能问题)。同时,你可以通过历史窗口,回到这个文件的任一版本。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/39/a15e57500c55e5285da5c712f5076839.png" alt="Fig15.2" title=":0Gclog 命令的效果">
|
||||
|
||||
要查看文件的某一行是谁最后修改的,可以使用 `:Git blame` 命令。你不需要给出当前文件的名字,而且 Fugitive 会把输出放到一个跟当前文件编辑窗口联动的新窗口里,并进行了色彩加亮。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/a6/67f1234d97b07e263b3f5yyyy75136a6.gif" alt="Fig15.3" title=":Git blame 命令的效果">
|
||||
|
||||
要比较当前文件和暂存区版本的区别,可以使用命令 `:Gvdiff`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9b/39/9bb1805d2b287cba0a415788c80b3639.png" alt="Fig15.4" title=":Gvdiff 命令的效果">
|
||||
|
||||
要查看当前 Git 库的状态,可以直接输入 `:Git` 命令。在结果缓冲区里,有很多正常模式命令可用(见 `:help fugitive-staging-maps`)。其中最实用的可能是:
|
||||
|
||||
- 使用 `s` 来把文件(整个文件)或光标下的修改(部分修改)加到暂存区中
|
||||
- 使用 `u` 来重置加入暂存区的修改(撤销 `s`)
|
||||
- 使用 `=` 来切换开关这个文件的内联 diff 显示
|
||||
- 使用 `o` 来在新分割的窗口中打开文件
|
||||
- 使用 `dv` 来比较文件和暂存区版本的区别
|
||||
- 使用 `cc` 来签入(commit)当前暂存区中的文件,相当于 `:Git commit` 命令
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ee/45/ee3479c8f61b666d7bea2077b9238f45.gif" alt="Fig15.5" title="在 :Git 命令后使用部分快捷键">
|
||||
|
||||
最后,`:Git push` 本来是不需要解释的,但你可能会高兴知道,在安装了 AsyncRun(见[第 8 讲](https://time.geekbang.org/column/article/271208))之后,这个操作自动就是异步的,不会在操作完成之前妨碍你继续进行编辑。
|
||||
|
||||
此外,所有的 `:Git …` 命令都可以打字成 `:G …`,事实上,之前给出的命令清单里,连这个空格都不要,所以你可以输入 `:Gpush` 来代替 `:G push`。不过,作者宣称 `:Gblame` 和 `:Gpush` 这样的命令在将来的版本里可能会被删除,所以你如果在用 `:Gpush` 这样的命令的话,可能需要知道一下这个事情。
|
||||
|
||||
你已经可以看到,Fugitive 的命令是相当复杂的,毕竟 Git 也很复杂。我上面讨论的,只是它的功能的一部分。你可以在它的帮助文件中了解进一步的信息(`:help fugitive`)。
|
||||
|
||||
## GitGutter
|
||||
|
||||
如果说 Fugitive 是一个全功能 Git 插件的话,[GitGutter](https://github.com/airblade/vim-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 的默认设置会出现突兀的边栏和无色的符号,丑得让人无法接受。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/cc/bd438ca715cfdea46cf1869f80522bcc.png" alt="Fig15.6" title="默认配色方案下的 GitGutter 效果">
|
||||
|
||||
### 使用
|
||||
|
||||
下面的动图展示了 GitGutter 的核心功能(使用 gruvbox 配色方案):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/87/6782e2ebd3216bb6d6681b620a9f0b87.gif" alt="Fig15.7" title="GitGutter 的功能展示">
|
||||
|
||||
简单说明一下:
|
||||
|
||||
- 在 Vim 的边栏里用 `+`、`-` 等符号和合适的配色来标注哪些行有了修改
|
||||
- `[c` 和 `]c` 可以用来跳转到上一个和下一个修改的位置
|
||||
- `<Leader>hp` 可以将光标下的修改块和缓存区中的内容进行对比
|
||||
- `<Leader>hs` 可以将光标下的修改块加入到暂存区中
|
||||
- `<Leader>hu` 可以恢复暂存区中的内容
|
||||
|
||||
换句话说,GitGutter 的重点是一个文件内修改的管理——这个功能 Fugitive 可以说也有(在 `:Git` 命令下管理),但不及 GitGutter 直观。使用 GitGutter,你可以在编辑文件时,立即选择把部分修改加入暂存区,方便后续的签入。因此,虽然看起来 GitGutter 的功能不多,我使用它的频度跟 Fugitive 差不多。
|
||||
|
||||
## Airline
|
||||
|
||||
如果你认为界面的美观很重要的话,你多半也会喜欢 [Airline](https://github.com/vim-airline/vim-airline)。Airline 应该可以算是 Vim 中最流行的插件之一了。它会和很多其他知名 Vim 插件自动进行集成,并在界面上展示非常丰富的信息。事实上,我在开篇词中给出的 Vim 界面截图中就使用了 Airline。我们这儿再重新展示一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d7/yc/d71beed8a4e1e2b970c63f7317655yyc.png" alt="Fig15.8" title="启用了 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](https://github.com/adobe-fonts/source-code-pro):一款设计非常优秀的字体,不花哨,但耐看
|
||||
- [Fira Code](https://github.com/tonsky/FiraCode):这是一款非常花哨的好字体(如果你疑惑上一张图里居然有“→”,就是这个字体对 `->` 的特殊渲染),也支持 Airline 需要的特殊符号
|
||||
- [DejaVu Sans Mono for Powerline](https://github.com/powerline/fonts/tree/master/DejaVuSansMono):著名的开源字体 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):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/5a/9a1afd2422ac186c6e835c0e6a545c5a.png" alt="Fig15.9" title="配置了显示缓冲区编号的 Vim 界面">
|
||||
|
||||
哦,对了,下面的那个闪电符号也是 Airline 搞出来了,提醒你当前的 Git 库里有没有提交的更改。
|
||||
|
||||
### 使用
|
||||
|
||||
Airline 的使用基本上是自动的,安装、配置完了,差不多就不用去管它了。留意看它的提示就行了。比如,它会在右下角进行可能错误的显示提示,包括行尾的空格(trailing space),YCM 发现的代码错误,等等。
|
||||
|
||||
Airline 还提供了一些命令,如 `:AirlineExtensions` 和 `AirlineToggle`。这些你直接看帮助文档(`:help airline`)就应该可以清楚了。
|
||||
|
||||
## NERDCommenter
|
||||
|
||||
我们前面已经介绍过了 NERDTree,而同一个作者写的另外一个插件 [NERDCommenter](https://github.com/preservim/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++ 代码来演示一下这些不同的注释类型:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/fd/503fb029feb4d00f511c5c270eccd4fd.gif" alt="Fig15.10" title="NERDCommenter 的多种不同注释类型">
|
||||
|
||||
目前 NERDCommenter 支持的文件类型也有一百种左右了,应该说还是非常全面的。对于调试代码,它也是一个非常重要的工具。
|
||||
|
||||
## Visual-Multi
|
||||
|
||||
我们今天要介绍的最后一个重要插件是 [Visual-Multi](https://github.com/mg979/vim-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 讲](https://time.geekbang.org/column/article/272988)综合实验里的那个批量代码更名操作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/79/d8652f26606c4549e331c9b951faf079.gif" alt="Fig15.11" title="使用 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 使用效率再上一层楼。我们今天的课后练习,就主要是温习一下学到的知识了:
|
||||
|
||||
1. 安装好这些插件(应该每个都很有用)
|
||||
1. 打开 Git 管理下的我们的配置文件,体验一下 Airline 的显示效果
|
||||
1. 随便做点修改,并存盘,观察 GitGutter 和 Airline 对边栏和状态栏的提示变化
|
||||
1. 使用 `:Git` 命令打开 Fugitive 面板,在修改的文件上用 `=` 观察其中的修改
|
||||
1. 使用 `s` 把修改加到 Git 的暂存区,然后用 `cc` 签入修改
|
||||
1. 关闭 Fugitive 面板,回到 .vimrc 或 _vimrc 文件,用 `:0Gclog` 浏览文件的历史,并查看这个文件的历史状态
|
||||
1. 用 `1<C-^>` 命令或 `:e .vimrc` 之类的命令回到原始文件,然后用 `:Git blame` 命令查看文件每行的修改历史
|
||||
1. 如果光标在左侧窗口,使用 `gq` 命令,如果光标在右侧,使用 `<C-W>o` 命令,回到只有配置文件的状态
|
||||
1. 使用 NERDCommenter 的命令,如 `<Leader>cb`,来修改代码行的注释状态
|
||||
1. 使用 Visual-Multi,把代码中的 `source` 全部改成 `so`
|
||||
|
||||
最后,如果你不知道怎么回退刚才对 Git 库的修改的话,可以手工输入下面的 Git 命令:
|
||||
|
||||
```
|
||||
git reset HEAD~1
|
||||
git reset --hard
|
||||
|
||||
```
|
||||
|
||||
在基本用法掌握之后,有学习余力的同学可以再仔细阅读一下帮助文件,了解我没有描述的用法。
|
||||
|
||||
如果有任何问题或疑问,欢迎留言和我讨论!
|
||||
164
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/16|终端和 GDB 支持:不离开 Vim 完成开发任务.md
Normal file
164
极客时间专栏/geek/Vim 实用技巧必知必会/提高篇/16|终端和 GDB 支持:不离开 Vim 完成开发任务.md
Normal file
@@ -0,0 +1,164 @@
|
||||
<audio id="audio" title="16|终端和 GDB 支持:不离开 Vim 完成开发任务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fe/42/fe5f7267ebd9f342d86c3a2c077d0042.mp3"></audio>
|
||||
|
||||
你好,我是吴咏炜。
|
||||
|
||||
早在 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** 电影里层层嵌套的世界一样。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/03/c384d46392e63ebbb162c7f8f888b103.png" alt="Fig16.1" title="开了两个终端窗口的 Vim,其中上面那个又再次运行 Vim">
|
||||
|
||||
当然,从实用的角度,我并不建议你这么做——那样可能会让人头昏,并且容易在使用 `<C-W>` 和 `:q` 这样的命令时,出现结果跟自己预想不一致的情况。
|
||||
|
||||
终端模拟器的行为应当跟普通的终端一致;因此在 Vim 的终端模拟器里,你可以直接使用的命令跟一般的 Vim 窗口很不一样。毕竟,你在终端模拟器里输入 `:` 时,肯定不是想进入 Vim 的命令行模式吧?这时候,你需要知道下面这些在“终端作业模式”下的特殊命令(完整列表见 [`:help t_CTRL-W`](https://yianwillis.github.io/vimcdoc/doc/terminal.html#t_CTRL-W_N)):
|
||||
|
||||
- `<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 里运行终端呢?
|
||||
|
||||
我是这么理解的:
|
||||
|
||||
1. **方便。**特别在远程连接的时候,有可能新开一个连接在某些环境里需要特别的认证,比较麻烦。即使连接没有任何障碍,你总还需要重新 cd 到工作目录里吧?而如果在一个现有的 Vim 会话里开一个新的终端,可以一个命令搞定,然后用你已经很熟悉的 Vim 命令在不同的窗口或标签页里切换。
|
||||
1. **文本。**我们可以从终端作业模式切换到终端普通模式,然后用我们熟悉的 Vim 命令来对缓冲区中的文本进行搜索、复制等处理工作。
|
||||
1. **控制。**你可以发送命令给终端,也可以读取终端屏幕上的信息。这样,事实上就打开了一片新天地,可以在 Vim 里做很多之前做不到的事情,比如,用 Vim 来比较两个屏幕输出的区别([`:help terminal-diff`](https://yianwillis.github.io/vimcdoc/doc/terminal.html#terminal-diff))。
|
||||
|
||||
终端窗口相关的函数名称都以 `term_` 打头(可以查看帮助文件 [`:help terminal-function-details`](https://yianwillis.github.io/vimcdoc/doc/terminal.html#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")
|
||||
|
||||
```
|
||||
|
||||
这当然不是完成这件任务的最好方法,但上面的代码展示了终端相关函数的一些基本用法:
|
||||
|
||||
1. 我们用 `term_start` 命令创建一个新的终端,得到终端缓冲区的编号
|
||||
1. 我们用 `term_wait` 等待 100 毫秒,待其就绪
|
||||
1. 我们用 `term_getcursor` 获取光标的当前行号
|
||||
1. 我们用 `term_sendkeys` 发送一个命令到终端上;ls 之后用 cat 是为了防止 ls 看到输出是终端而产生多列的输出
|
||||
1. 然后我们等待命令执行完成并更新终端
|
||||
1. 我们获取光标的当前位置,然后用 `term_getline` 获得上一次的行号和这一次的行号之间的行的内容,放到变量 `result` 里
|
||||
1. 我们然后发送一个 `<C-D>` 到终端,结束作业
|
||||
1. 然后我们等待到 `term_getstaus` 返回的状态成为 `'finished'`,即终端作业已经执行结束
|
||||
1. 最后我们用缓冲区编号加 `bd` 命令删除缓冲区(所以屏幕上我们看不到这个终端窗口),并用换行符作为分隔符打印 ls 返回的内容
|
||||
|
||||
你可以实际测试一下这个脚本,体会一下这些基本功能。比如,可以把脚本存盘为 test.vim,然后用 `:so %` 来运行。
|
||||
|
||||
## GDB 支持
|
||||
|
||||
为什么 Vim 直到最近才支持 GDB 呢?因为这真不是件容易的事情啊。为了能在 Vim 里顺畅地使用 GDB,Bram 需要在 Vim 里实现下面这些不同的功能:
|
||||
|
||||
- 终端支持
|
||||
- 作业(job)和通道(channel)
|
||||
- 窗口工具条、弹出窗口和弹出式菜单
|
||||
|
||||
有了这些功能之后,Vim 通过一个内置的插件,就可以提供 GDB 的调试支持了。我们可以通过 `:packadd termdebug` 命令来加载这个插件,然后通过 `:Termdebug 可执行程序名称` 来调试一个可执行程序。
|
||||
|
||||
下面这个动图可以说明最主要的流程:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/a4/493d863ebaa07a528b484e2d01093fa4.gif" alt="Fig16.2" title="在 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 的话,这个功能还是有很大的易用性提升的。
|
||||
|
||||
最后,同样地,如果有任何问题或疑问,欢迎留言和我讨论!
|
||||
|
||||
我是吴咏炜,让我们在告别这个课程之前,再道一次再见。
|
||||
Reference in New Issue
Block a user