This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
<audio id="audio" title="拓展1纯文本编辑使用 Vim 书写中英文文档" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9e/72/9e22f35aa329f9594fdc49772a7d2772.mp3"></audio>
你好,我是吴咏炜。
今天是拓展篇的第 1 讲,我想带你对 Vim 的纯文本编辑技巧做一个专项突破。由于 Vim 是在欧美世界诞生的工具,贡献者中也是说英语的人居多,因而它对英文的支持要远远超出其他语言。所以今天,我们就深入讨论一下,如何使用 Vim 来进行纯文本编辑,特别是英文的文本编辑。
熟练掌握这一讲的内容,可以让你使用 Vim 书写中英文文档时都感到游刃有余。如果你有这个需求,一定要亲自动手尝试我提到的这些功能,加深自己的记忆。如果你觉得还需要多花一点时间,消化吸收前几讲的基础知识,也可以先阅读全文,把握要点,之后再回过头来深入学习。
## 为什么不使用字处理器?
你可能已经开始怀疑了,我为什么要使用 Vim 来进行文字编辑?用 Word 不香么?如果嫌 Word 贵,还有免费的 WPS 啊……
首先Word 和 WPS 这些字处理器不是用来生成纯文本文件的。在处理纯文本文件上,它们反而会有诸多劣势,如:
- 只能本地使用,既不能在远程 Linux 服务器上运行,也不能用 SSH/SCP 的方式打开远程的文件(除非在服务器上启用 Samba 服务,但体验真的不好)
- 分段和分行一般没有很好的区分
- 如果存成纯文本的话,格式会全部丢失
最后一句话似乎是废话?还真不是,纯文本文件里面是可以存储格式的,但 Word 和其他字处理软件对于文本类型一般只能支持纯文本或富文本Rich Text而富文本虽然包含了格式信息但却对直接阅读不友好。我想没有人会去手写富文本文件吧。仍有一些带格式的文本文件比较适合手写下面这些是其中较为流行的格式
- [Tex](https://zh.wikipedia.org/zh-cn/TeX) 和 [LaTeX](https://zh.wikipedia.org/zh-cn/LaTeX),著名的特别适合写公式的文档系统,在数学和物理学界尤其流行。
- [DocBook](https://zh.wikipedia.org/zh-cn/DocBook),基于 SGML/XML 的文档系统,可以生成多种不同的输出格式;大量开源软件的文档是用 DocBook 写的。
- [AsciiDoc](https://en.wikipedia.org/wiki/AsciiDoc),功能和 DocBook 等价、但使用非 XML 的简化语法的文档系统;有的国外技术书出版社接受作者用这种格式提交的稿件。
- [HTML](https://zh.wikipedia.org/zh-cn/HTML)HTML 的阅读友好性一般,但胜在熟悉的人多。
- [Markdown](https://zh.wikipedia.org/zh-cn/Markdown)Markdown 的阅读体验非常友好,因而它虽然最“年轻”却最流行。接下来,我们就介绍一下这种文件类型。
### Markdown 简介
Markdown 是由 John Gruber约翰 · 格鲁伯)在 2004 年发明的,它不是一种标准化的格式,存在着多个实现,功能也并不完备。尽管如此,由于它轻量、易写、易读,很快就在互联网上流行开了。在 GitHub 上,现在 README 文件一般都使用 Markdown。
<img src="https://static001.geekbang.org/resource/image/0d/da/0dcb5396yy16e6bc19598c4984bb98da.png" alt="FigX1.1" title="GitHub 上的显示效果和 Markdown 源码">
通过工具(很多是开源的,如 [pandoc](https://pandoc.org/)Markdown 可以很容易地转成网页、PDF 等其他格式,同时也很适合以纯文本的形式阅读。而 HTML、DocBook 等格式实际上是不太适合人直接看源代码来阅读的。此外,极客时间,以及很多写作平台,用的也是 Markdown。
事实上在处理代码相关的文档时Word 还真没有 Markdown 方便。Word 的某些“自动”功能,如把直引号替换为弯引号,会对键入代码造成干扰。而对嵌入的代码进行语法加亮,则是 Word 里没有、而 Markdown 里非常成熟的功能了……
不过,我们今天的重点不是 Markdown而是怎么使用 Vim 来编辑上面提到的所有文件格式。没有了图形界面的简单化处理你可以把控文本的一切细节但同时Vim 的语法加亮和文字编辑功能本身,也会让你编辑起来非常得心应手。鉴于 Markdown 的优势,我们今天的例子还是会用 Markdown。这样你学了这一讲之后至少会知道怎么高效地给开源项目写个 README 文件。
## 英文文本编辑
考虑到 Markdown 等标准在中文处理的标准化上面有先天不足,我们先学习文档的主语言为英文的情况。我们可以先看一眼下面的截图:
<img src="https://static001.geekbang.org/resource/image/f5/4a/f5423a51e057de61b6fbe967954fcd4a.png" alt="FigX1.2" title="一个 README 文档的开头部分">
然后对比一下它的 Markdown 源代码在 Vim 中的展示效果:
<img src="https://static001.geekbang.org/resource/image/d6/df/d6d6f6fd30505cbe3241f82fc1a4c6df.png" alt="FigX1.3" title="该 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.&nbsp;.&nbsp;.&nbsp;. The 66-character line .&nbsp;.&nbsp;. is widely regarded as ideal.
我们说列宽 72是指最大值而使用 72 产生的实际文本宽度,差不多就是落在 66 这个理想值附近了。一些编码规范,如 Python 的 [PEP 8](https://www.python.org/dev/peps/pep-0008/),也约定对文档内容的列宽数值应当是 72。[大部分编码规范对代码宽度的约定稍宽松些,一般是 79 或 80。](https://en.wikipedia.org/wiki/Characters_per_line#In_programming)
### 格式化选项
我们上一讲已经提到格式化选项 `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 &lt;CR&gt; 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` 选项来加亮行尾空格和行尾结束符):
<img src="https://static001.geekbang.org/resource/image/1d/3e/1d015285b9968f41881ace309d944a3e.gif" alt="FigX1.4" title="对使用了 a 和 w 两个 formatoptions 的文本进行编辑">
### 段中不换行的文本
到现在为止,我们讨论的英文文本编辑,基本都是行尾结束符不代表真正分行的情况。这种方式最常见,但也存在例外,如有些网站不使用标准的 Markdown 规则,把行尾结束符直接就当成换行了。在这样的情况下,我们就不应该在一段中间手工插入换行符。不过,由于这种方式不是 Vim 的“自然”处理方式,我们需要修改一些选项和处理习惯来应对这种情况。
首先Vim 在默认配置下会在窗口宽度不足时自动折行显示,但不会对折行的位置进行特殊挑选,很可能会在单词的中间折行。我们可以设置选项 `linebreak`,告诉 Vim 要在 `breakat` 的字符上才可以折行。默认的 `breakat` 设置包含了空白字符和英文标点符号,因此直接可以在英文环境中进行使用。下图展示了 `linebreak` 的效果:
<img src="https://static001.geekbang.org/resource/image/32/3f/323652ccf7757cdbae4dc6b9b6ff913f.gif" alt="FigX1.5" title="设置 linebreak 的效果">
在这种模式下进行编辑时,另外一个要注意的问题是 `j``k` 移动的是一个物理行,而非屏幕行。这和很多编辑器的行为是不同的。要让光标一次移动一个屏幕行,需要的按键是 `gj``gk`。如果你希望 `&lt;Up&gt;``&lt;Down&gt;` 的行为跟主流的编辑器一致,可以考虑以下的设定:
```
" 修改光标上下键一次移动一个屏幕行
nnoremap &lt;Up&gt; gk
inoremap &lt;Up&gt; &lt;C-O&gt;gk
nnoremap &lt;Down&gt; gj
inoremap &lt;Down&gt; &lt;C-O&gt;gj
```
最后,如果之前换行符不代表分段,现在你希望换行即分段,你可以用 `J`(代表 join连接命令把多行重新连接成一行。这个命令在某种程度上可以看作是 `gq` 的逆命令。由于这个命令根据字符的类型来决定是否插入空格和插入几个空格(参考 [`:help 'joinspaces'`](https://yianwillis.github.io/vimcdoc/doc/options.html#'joinspaces')),它并不能简单地用替换命令来代替。如果考虑最简单的情况(`:set nojoinspaces`),那至少在 `formatoptions` 中含有 `w` 时,我们可以用下面的替换命令来连接同一段的所有行:
```
:%s/\([^\n]\)\s\+\n\s*\([^\n]\)/\1 \2/
```
事实上,使用 `joinspaces` 和 Markdown 的双行尾空格代表换行是潜在会冲突的。在 Markdown 中使用 `w` 时,应当使用 `nojoinspaces`
### 模式行
到现在为止我们讨论的好些选项不仅不适合作为全局选项也不适合作为某一文件类型的选项而更适合用作单个文件的选项。Vim 也确实提供了这样的功能,叫做模式行([`:help modeline`](https://yianwillis.github.io/vimcdoc/doc/options.html#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`](https://yianwillis.github.io/vimcdoc/doc/change.html#format-comments),我这儿就不展开了。
### 拼写检查
写英文时启用自动拼写检查这是一个写作的好习惯。哪怕你英语很好也可能因为疏忽而拼错。Vim 从版本 7 开始,就内置了拼写检查的功能,可以通过选项 `spell` 来打开。
<img src="https://static001.geekbang.org/resource/image/a8/48/a86c3c425993333ecb38dd5c7205f148.png" alt="FigX1.6" title="开启了拼写检查的 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`](https://yianwillis.github.io/vimcdoc/doc/spell.html#spell))。
Vim 拼写检查的默认语言是英语。对不同语言的支持,我们可以使用 Vim 选项 `spelllang` 来设定。比如,如果你希望按照英式英语的拼写,那你可以设置:
```
set spelllang=en_gb
```
这样一来Vim 就会把非英式英语的拼写方式用绿色波浪线(当然,这和色彩方案有关)标出来。在图形界面 Vim 里,你同样可以很方便地用右键点击来更改:
<img src="https://static001.geekbang.org/resource/image/58/8a/58fcc5168d7dae4d2b59d05327a0068a.png" alt="FigX1.7" title="标出不在 spelllang 里面的拼写方式和右键菜单">
你如果希望使用美式英语,当然使用 `:set spelllang=en_us` 就可以了。同时使用英式和美式,可以用 `:set spelllang=en_gb,en_us`。如果任何英语拼写都能接受,那使用默认值或 `en` 就行。
作为东亚文字的特殊情况,如果你希望所有的东亚字符不被标成拼写错误的话,可以在 `spelllang` 选项里使用特殊值 `cjk`。作为中国人,我们可能会需要这么用:
```
set spelllang+=cjk
```
### 拼写完成
我们之前提到了 Vim 支持用一个字典文件来进行拼写完成,你可以在帮助 [`:help 'dictionary'`](https://yianwillis.github.io/vimcdoc/doc/options.html#'dictionary') 里找到相关的信息。不过,这种方式需要你手工去寻找一个字典文件,并在 vimrc 里进行配置,不那么方便。更简单的方式,是**先启用拼写检查**,然后正常使用拼写完成的快捷键 `&lt;C-X&gt;&lt;C-K&gt;` 即可。如果你完整拼出了单词,但 Vim 提示拼错了,你也可以使用快捷键 `&lt;C-X&gt;s` 来使用和查看拼写建议。
**注意:**虽然 `&lt;C-S&gt;` 在图形界面 Vim 里可以使用,但终端(不是 Vim可能会解释 `&lt;C-S&gt;` 为特殊控制字符,因而我们一般不使用 `&lt;C-X&gt;&lt;C-S&gt;`。如果你发现一不小心键入 `&lt;C-S&gt;` 导致终端表现得像失去响应一样,一般可以用 `&lt;C-Q&gt;` 来恢复。
## 中文文本编辑
跟英文文本编辑类似,中文处理同样有段中有断行和段中无断行两种方式。如果段中有断行,中文的主要处理麻烦是在转换成 HTML 或其他格式时通常不应该把行尾结束符转换成空格。我目前测下来GitHub 的 Markdown 能有这样的合理行为,但很多其他工具,如 pandoc则没有对中文作这样的特殊处理。因此中文文档还是不在段中进行断行更保险一些。
这两种文本组织方式 Vim 都是能处理的,其方式和英文文本编辑差不多,差别主要表现在下面两点。
首先,英文段中不分行时我们推荐使用 `linebreak` 选项,但中文段中不分行时我们则不推荐启用这个选项。因为让 Vim 挑空格位置来折行,反而会让中文文本显得乱、不好看。下面的图展示了区别:
<img src="https://static001.geekbang.org/resource/image/86/24/868bce15c9b6e7d518d0407bc9acb724.png" alt="FigX1.8" title="使用选项 nolinebreak 的效果">
<img src="https://static001.geekbang.org/resource/image/d2/2b/d2f33880359fb244ab51d6bd1e49d62b.png" alt="FigX1.9" title="使用选项 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 这个版本引入的。你可以通过下面的截图看下这个标志的效果:
<img src="https://static001.geekbang.org/resource/image/ce/3c/ce422850005012506678c34ea9a9c93c.png" alt="FigX1.10" title="不同 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内容不限可以从其他地方复制。要求是
1. 内容除标题外至少三段,每段至少两行
1. 使用模式行控制选项
1. 段中分行,启用自动格式化但不使用行尾空格的方式
1. 尝试在该文件中嵌入代码
1. 想一想,为什么 `a` 选项一般和 `w` 选项一起使用
你在写 README 文件,或者其他文本文档时,还遇到过其他问题吗?请及时和我交流。你也可以把今天这一讲的内容,分享给身边其他需要编辑纯文本文件的朋友。
我是吴咏炜,我们下一讲再见!

View File

@@ -0,0 +1,266 @@
<audio id="audio" title="拓展2C 程序员的 Vim 工作环境C 代码的搜索、提示和自动完成" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ae/87/ae05168e382885a2f392ed1f9b15fc87.mp3"></audio>
你好,我是吴咏炜。
从今天开始,我们会用连续 3 讲,深入讨论怎么为编程定制环境。如果你是 C 程序员,那么今天这一讲对你来说毫无疑问是必修课。如果你用的是类 C 语言也能从这一讲中学到很多有用的内容尤其是在语法加亮精调、tags 和 Clang-Format 部分。
## 语法加亮精调
在[第 4 讲](https://time.geekbang.org/column/article/267765)中我们已经学到了Vim 能根据文件类型对代码进行加亮。在[第 12 讲](https://time.geekbang.org/column/article/275752)里,我们还进一步讨论了 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`](https://yianwillis.github.io/vimcdoc/doc/syntax.html#ft-c-syntax)。
## Tags
我们在[第 8 讲](https://time.geekbang.org/column/article/271208)里已经讨论过了 tags 文件。回想一下tags 是 ctags 工具分析你的代码之后生成的可读文本符号数据库,帮助你找到一个符号的定义所在位置。这个 tags 格式有点简单、有点平却也正好像极了 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
" 执行 ctagssilent 会抑制执行完的确认提示
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()`](https://yianwillis.github.io/vimcdoc/doc/eval.html#expand()))。代码中的其他内容你应当可以理解了。
如果你觉得上面这样做太麻烦的话,有个好消息是已经有人把类似于上面、但更加完善的功能做成了插件。插件 [ludovicchabant/vim-gutentags](https://github.com/ludovicchabant/vim-gutentags) 可以完成 tags 文件的管理工作。如果你希望 tags 文件能自动更新的话,这个插件很可能可以满足你的所有需求:基本上你只需要配置它的选项,而不需要自己写 Vim 脚本代码。
不过,如果你在一个大项目上工作,代码很少发生结构性更动的话,也许在 shell/cron 里定期执行 ctags 命令,是个最简单、最不对开发者造成干扰的选项。毕竟,靠编辑器触发 ctags 命令也不那么可靠——因为很多其他操作(比如 git 切换分支)之后,你也同样需要更新 tags。
## EchoFunc
记得我们刚刚讲过在 tags 文件里加入函数签名信息吧下面我们就要用上了。EchoFunc 插件可以用来回显函数的原型。
首先我们需要安装 [EchoFunc](https://github.com/mbbill/echofunc),使用包管理器安装 mbbill/echofunc 即可。然后,如果你有正确的 tags 文件,现在当你输入函数名加 `(`Vim 就会在屏幕底部自动提示函数的原型了:
<img src="https://static001.geekbang.org/resource/image/da/86/daa92d8a62f2d32c351f5b8897ff0d86.png" alt="FigX2.1" title="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](http://cscope.sourceforge.net/) 的支持,因此我们今天就讨论一下 Cscope。
根据 Cscope 的文档,它的定位是一个代码浏览工具,最主要的功能是代码搜索,包括查找符号的定义和符号的引用,查找函数调用的函数和调用该函数的函数,等等。查找引用某符号的地方、调用某函数的地方、包含某文件的地方,就是 Cscope 的独特之处了。
### 安装和配置
用 Cscope 之前,我们首先得把它装起来。我们先要安装 Cscope 本身:
- Ubuntu 下可使用 `sudo apt install cscope`
- CentOS 下可使用 `sudo yum install cscope`
- macOS 下可使用 `brew install cscope`
- Windows 下可在 [cscope-win32](https://code.google.com/archive/p/cscope-win32/downloads) 项目里下载 cscope.exe 的可执行文件,然后放到 PATH 设定里的某一个目录下即可
然后我们需要映射使用 Cscope 的按键。Cscope 的网站上提供了一个映射的脚本,不过,它里面用到了 `&lt;C-Space&gt;`,这个快捷键在很多系统上是会有问题的。因此我替换了一下,你可以安装 adah1972/cscope_maps.vim 来使用它。现在:
- 使用 `&lt;C-\&gt;` 加 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``&lt;C-W&gt;f` 命令相似)
- `i`查找包含include这个文件的文件
- `a`查找一个符号被赋值assigned的地方
比如,我们在 Vim 的源代码里要查找 `vim_free` 函数的定义,只需要键入命令 `:cscope find g vim_free`,或者把光标移到符号 `vim_free` 上,然后按下 `&lt;C-\&gt;g`。在目前的配置下,使用 `&lt;C-]&gt;` 也可以,因为 cscope_maps.vim 脚本里设置了 `:cscopetag` 选项(参见帮助 [`:help cscopetag`](https://yianwillis.github.io/vimcdoc/doc/if_cscop.html#cscopetag))。
如果我们想要使用分割窗口,那对应的命令是 `:scscope find g vim_free`,光标已经在符号上时的按键是 `|g``&lt;C-W&gt;]`。如果我们想要垂直分割窗口,那命令是 `:vert scscope find g vim_free`,光标已经在符号上时的按键是 `||g`
其他的 Cscope 命令也类似,把 `g` 替换成相应的命令即可。值得提一下,虽然 `t``e` 命令在 Vim 里可以用 `:grep` 命令替代,但这两个命令执行起来要比 `:grep` 快。当然Cscope 的真正优势还是 `s``c``i``a` 这样的命令。下面展示的是在一个 Vim 源文件中执行符号引用查找(`s`)的结果,可以看到比 `:grep` 还是方便快捷了很多的:
<img src="https://static001.geekbang.org/resource/image/90/51/903b05c04b1eece192995f849791b051.gif" alt="FigX2.2" title="在 Vim 源代码中查找一个符号的引用">
## ClangComplete
下面我们来讨论一下一个新话题C 代码的自动完成。
众所周知Clang 是一个目前很流行的、模块化的 C/C++ 编译器。它跟其他 C/C++ 编译器最不一样的地方,是它让其他程序能够很容易利用 Clang 对源代码的处理结果。目前很多对 C/C++ 源代码进行处理的工具,都是基于 Clang 来开发的。[ClangComplete](https://github.com/xavierd/clang_complete) 也是其中的一个,它在 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](https://www.softwarecollections.org/en/scls/rhscl/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 里得到的结果:
<img src="https://static001.geekbang.org/resource/image/4e/fd/4ea2742bacb5ab5a59d1d4f2aa9bcdfd.gif" alt="FigX2.3" title=" ClangComplete 的效果">
我们可以看到,现在 Vim 知道了 `tm` 是个结构的指针,并且知道指针的成员有哪些。
这个例子比较简单,如果我们在命令行上进行编译的话,不需要任何特殊参数。如果我们命令行上需要参数,那很可能 ClangComplete 也需要知道这些参数,才能正确工作。这些参数信息应该放在文件所在目录或其父目录下的名为 .clang_complete 的文件里。比如,我的[极客时间 C++ 课程的示例代码里就有这个文件](https://github.com/adah1972/geek_time_cpp/blob/master/.clang_complete),内容也很简单:
```
-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 用户建议直接安装[官方提供的下载版本](https://releases.llvm.org/download.html)。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](https://www.softwarecollections.org/en/scls/rhscl/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 所在的目录下找不到的话,也没关系。你可以直接从[网上下载](https://github.com/llvm-mirror/clang/blob/master/tools/clang-format/clang-format.py),放到你自己知道的一个位置。
我们如果把这个脚本的位置记作 /path/to/clang-format.py那我们现在在 vimrc 配置里加上这行以后就能工作了:
```
noremap &lt;silent&gt; &lt;Tab&gt; :pyxf /path/to/clang-format.py&lt;CR&gt;
```
我是映射了 `&lt;Tab&gt;` 在正常模式和可视模式下对代码进行格式化。如果正常模式的话,我们是对当前语句执行格式化。如果可视模式,那就是对选定的行进行格式化了。这些应该都非常自然了。
Clang-Format 使用[规则配置文件](https://clang.llvm.org/docs/ClangFormatStyleOptions.html)来确定如何进行格式化。首次配置觉得复杂的话,你可以参考我的[配置文件](https://github.com/adah1972/nvwa/blob/master/.clang-format)。具体的效果你可以根据你的项目要求来调整。这个配置文件 .clang-format 同样是放在你的源代码文件目录下或其某一父目录下。
格式化的过程可以参考下面的动画:
<img src="https://static001.geekbang.org/resource/image/5d/52/5d7e52d307ba3d7393af2efbaaf74852.gif" alt="FigX2.4" title="在 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 项目中实际操练几下,做到真正会用和真正去用。如果遇到任何问题,欢迎留言和我交流。
我是吴咏炜,我们下一讲再见!

View File

@@ -0,0 +1,206 @@
<audio id="audio" title="拓展3Python 程序员的 Vim 工作环境:完整的 Python 开发环境" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/99/dd/9937800e8c2500d732c24898d435b7dd.mp3"></audio>
你好,我是吴咏炜。
今天这一讲,我会介绍 Python 程序员定制 Vim 工作环境的完整方法。
Python 的流行程度越来越高Python 程序员们对此一定是很高兴的。在 Stack Overflow 的 2020 年开发者调查里Python 在最受爱戴most loved的语言里排名第三而在最想要most wanted的语言里则已经连续四年排名第一因此它在 Vim 的生态系统里受到了良好的支持,也不会是件令人吃惊的事。有开发者已经把 Python 开发所需要的若干插件揉到了一起组成了一套开箱即用的工具python-mode。
今天我们就以它为基础,讨论一下 Vim 对开发 Python 提供的支持。
## 功能简介
[Python-mode](https://github.com/python-mode/python-mode) 实际上是以 Vim 插件形式出现的一套工具,它包含了多个用于 Python 开发的工具。根据官网的介绍,它的主要功能点是:
- 支持 Python 3.6+
- 语法加亮
- 虚拟环境支持
- 运行 Python 代码(`&lt;leader&gt;r`
- 添加/删除断点(`&lt;leader&gt;b`
- 改善了的 Python 缩进
- Python 的移动命令和操作符(`]]`, `3[[`, `]]M`, `vaC`, `viM`, `daC`, `ciM`, …)
- 改善了的 Python 折叠
- 同时运行多个代码检查器(`:PymodeLint`
- 自动修正 PEP 8 错误(`:PymodeLintAuto`
- 自动在 Python 文档里搜索(`K`
- 代码重构
- 智能感知的代码完成
- 跳转到定义(`&lt;C-c&gt;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 &lt;&lt; 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。不过需要的话我们仍可以通过 `&lt;C-X&gt;&lt;C-O&gt;` 来使用 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
```
效果见下图:
<img src="https://static001.geekbang.org/resource/image/38/e0/386b3e6b2b34b06180138byyfde72de0.png" alt="FigX3.1" title="代码折叠效果">
这个功能会导致打开 Python 文件变慢。你可以试试,斟酌一下自己是否希望使用这个功能。
### 快速文档查阅
Python-mode 默认映射了 `K` 对光标下的单词进行文档查阅。跟其他查阅文档的方式比起来,这还是非常快捷方便的。
<img src="https://static001.geekbang.org/resource/image/f4/ef/f4f098ca5d3f25177d925b421884bdef.png" alt="FigX3.2" title="使用 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 也会自动在你保存文件时进行检查。
<img src="https://static001.geekbang.org/resource/image/f2/93/f2112148213d15682ff2a7abf4e1db93.gif" alt="FigX3.3" title="我几年前写的不符合 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 之后,你就可以使用下面的命令了:
- 使用 `&lt;C-X&gt;&lt;C-O&gt;` 来启用自动完成(我们把 `.` 还是交给 YCM 了)
- 使用 `&lt;C-C&gt;g` 来跳转到定义(跟 YCM 的 `\gt`大部分情况下没区别rope 跳转更好和 YCM 跳转更好的情况都有,但都不多见)
- 使用 `&lt;C-C&gt;d` 来查看光标下符号的文档;和 `K` 键不同,这个命令可以查看当前项目代码里的文档字串
- 重构refactor功能以 `&lt;C-C&gt;r` 开始,如 `&lt;C-C&gt;rr` 是重命名rename光标下的符号这些功能还是比较强大的可以使用 `:help pymode-rope-refactoring` 来查看完整的帮助信息)
下面的动图展示了 rope 的若干功能:
<img src="https://static001.geekbang.org/resource/image/ec/00/ecfd3462bcd9065660930a9986134f00.gif" alt="FigX3.4" title="在 rope 里查看文档、跳转到定义和重命名">
## 替换方案
如果你对 python-mode 的某些功能不满意,可以禁用其部分功能,用其他插件来代替。
首先,如果你如果觉得 rope 提供的额外功能对你用处不大的话,我们可以完全禁用 rope`let g:pymode_rope = 0`),专心使用 YCM。这样硬盘上也就不会出现 .ropeproject 那样的目录了。
其次,如果你真的希望能在写代码的时候自动进行 pylint 检查,那你也可以禁用 python-mode 里的代码检查器功能(`let g:pymode_lint = 0`),转而使用 [ALE](https://github.com/dense-analysis/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 装起来、配置好、用一下。如果遇到什么问题,欢迎留言和我讨论。
我是吴咏炜,我们下一讲再见!

View File

@@ -0,0 +1,313 @@
<audio id="audio" title="拓展4 | 插件样例分析:自己动手改进插件" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/88/b9/88247b3cd8ccd19b4b277eb879311ab9.mp3"></audio>
你好,我是吴咏炜。
上一讲,我们对 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('&lt;sfile&gt;:p:h:h') . '/ycm_extra_conf.py'
```
这个差不多是个最简单的插件了吧。Vim 脚本里只做了一件事,设置全局变量 `g:ycm_global_ycm_extra_conf` 给 YCM 用。关于脚本中的 `expand` 函数,我们稍微展开一下:
- `expand` 是用来展开文件名的。参数字符串里如果有 `%`,就代表当前编辑的文件名;如果有 `&lt;sfile&gt;`,代表当前执行的源代码文件(其他可展开的名字请参见 [`:help expand()`](https://yianwillis.github.io/vimcdoc/doc/eval.html#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](https://time.geekbang.org/column/article/277058) 里我给出了一个自己改过的 cscope_maps.vim我们现在就来看看它的原始版本然后看一下怎么修改它的行为。
原始版本在 [Cscope 的网站](http://cscope.sourceforge.net/cscope_maps.vim)上。可以看到,这也是一个比较简单的 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 &lt;C-\&gt;s :cs find s &lt;C-R&gt;=expand("&lt;cword&gt;")&lt;CR&gt;&lt;CR&gt;
```
这个键映射把 `&lt;C-\&gt;s` 映射成了一个 `:cs find s …` 命令。值得注意的是命令的后半截:
- 脚本里使用 `&lt;C-R&gt;=…&lt;CR&gt;` 来执行一个 Vim 表达式,并把结果填到命令里。
- 我们又一次见到了 `expand` 函数。这一次,要展开的是 `&lt;cword&gt;`,即当前光标下的单词。
- 注意结尾两个 `&lt;CR&gt;` 里第一个是给 `&lt;C-R&gt;=` 的,第二个才是执行命令的回车键。
让我有意见的是下面这样的键映射:
```
nmap &lt;C-@&gt;s :scs find s &lt;C-R&gt;=expand("&lt;cword&gt;")&lt;CR&gt;&lt;CR&gt;
```
这儿用 `&lt;C-@&gt;` 代表 Ctrl-空格,而这个组合键在很多系统上不可用。既然已经使用了 Ctrl-\ 作为 Cscope 的专用起始键,我觉得继续用 Shift-\ 就好。由于 `|` 在 Vim 里用来分隔多个语句,这儿我们要换个写法,改成:
```
nmap &lt;bar&gt;s :scs find s &lt;C-R&gt;=expand("&lt;cword&gt;")&lt;CR&gt;&lt;CR&gt;
```
我的完整修改过程,可以查看:
[https://github.com/adah1972/cscope_maps.vim/commits/master](https://github.com/adah1972/cscope_maps.vim/commits/master)
总的来说,这也是个非常小、非常轻松的修改。
## EchoFunc
事实上,大部分行为良好的插件会允许用户通过一些全局变量来定制键映射之类的设定。不过,对于没有提供这种定制性的插件,我们自己找到代码里的键映射语句,手工修改一下,也是一种可能发生的常见情况。比如,[EchoFunc](https://github.com/mbbill/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='&lt;M-=&gt;'
endif
endif
if !exists("g:EchoFuncKeyPrev")
if has ("mac")
let g:EchoFuncKeyPrev='±'
else
let g:EchoFuncKeyPrev='&lt;M--&gt;'
endif
endif
```
在我这儿给出的三个全局变量的相关定义里第一个是对起泡提示的数量限制第二个是下一个函数的键定义第三个是上一个函数的键定义。在后两个键定义里还分平台Mac 或非 Mac进行了不同的设置。这些都是非常直接了当的。
如果我们在 `EchoFuncKeyNext` 上面按下 `*` 来搜索这个变量的使用,我们就会发现它们是在函数 `EchoFuncStart` 里被真正使用的:
```
if maparg(g:EchoFuncKeyNext, "i") == '' &amp;&amp; maparg(g:EchoFuncKeyPrev, "i") == ''
exec 'inoremap &lt;silent&gt; &lt;buffer&gt; ' . g:EchoFuncKeyNext . ' &lt;c-r&gt;=EchoFuncN()&lt;cr&gt;'
exec 'inoremap &lt;silent&gt; &lt;buffer&gt; ' . g:EchoFuncKeyPrev . ' &lt;c-r&gt;=EchoFuncP()&lt;cr&gt;'
endif
```
这儿的代码说的是:
- 如果 `g:EchoFuncKeyNext``g:EchoFuncKeyPrev` 描述的键映射([`:help maparg()`](https://yianwillis.github.io/vimcdoc/doc/eval.html#maparg()))在插入模式(`"i"`)没有被占用(`== ''`)的话,那我们就执行(`exec`)针对当前缓冲区(`&lt;buffer&gt;`)的插入模式键映射(`inoremap`),让其安静地(`&lt;silent&gt;`)执行(`&lt;c-r&gt;=`)函数中的语句。
注意,在键映射中使用 `&lt;C-R&gt;=` 来执行语句是一种常用技巧。这种情况下,我们常常不是要获得函数返回的结果(所以这些函数通常返回 `''`),而只是需要执行一些指定的代码,产生需要的“副作用”。在这儿,我们需要的副作用就是选择函数列表里的下一项和上一项了。
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 &amp;Tools.Echo\ F&amp;unction.Echo\ F&amp;unction\ Start :call EchoFuncStart()&lt;CR&gt;
menu &amp;Tools.Echo\ F&amp;unction.Echo\ Function\ Sto&amp;p :call EchoFuncStop()&lt;CR&gt;
endif
if has("balloon_eval")
autocmd BufRead,BufNewFile * call CheckedBalloonDeclarationStart()
if has('gui_running')
menu &amp;Tools.Echo\ Function.&amp;Balloon\ Declaration\ Start :call BalloonDeclarationStart()&lt;CR&gt;
menu &amp;Tools.Echo\ Function.Balloon\ Declaration\ &amp;Stop :call BalloonDeclarationStop()&lt;CR&gt;
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 日志里是有说明的:
>
<p>1) fix key “(” “)” mapping conflict with other plugins:<br>
&nbsp;<br>
first, move plugin folder into after/ folder, so that echofunc will be load after most plugins have been loaded<br>
&nbsp;<br>
Second, if during initialization time, if it find “(” or “)” key have been mapped, it will try to append &lt;Plug&gt;EchofuncXX function to it.</p>
因为它用到 `(``)` 作为键映射,容易和其他插件冲突,因此它会最后加载,并尽量把自己键映射补充进去。
对于插件功能本身的特殊逻辑,我就不解释啦。
## arm-syntax-vim
今天最后一个插件样例,是我最近的一个实际需求。由于我写的代码需要最终跑在 ARM 平台上,我偶尔需要检查一下产生的 ARM 汇编代码。在 Vim 的默认配置下,产生的汇编代码效果不太理想,如下图所示:
<img src="https://static001.geekbang.org/resource/image/c0/04/c0865cfb19f614e6bc1fdaeca55b6504.png" alt="FigX4.1" title="使用 asm 语法类型显示的 ARM 汇编">
这里最糟糕的地方是,`stmfd` 那行里的 `{r4, lr}` 居然显示成了注释?是可忍,孰不可忍!
还好,我用不着从头开始自己搞。网上略加搜索,我就找到了 [ARM9/arm-syntax-vim](ARM9/arm-syntax-vim) 这个 Vim 脚本,可以获得好得多的效果,如下所示:
<img src="https://static001.geekbang.org/resource/image/02/b6/02c7aa2c55ec800d11935d7a810b0db6.png" alt="FigX4.2" title="使用 arm 语法类型显示的 ARM 汇编">
不过,这个脚本还是缺了点东西,它只包含了语法文件,不能把 GCC 产生的 .s 文件识别为它支持的 arm、armv4 和 armv5 格式。我要做的就是添加文件类型识别,让 Vim 把 ARM 的汇编文件识别成合适的 ARM 格式。
在[第 8 讲](https://time.geekbang.org/column/article/271208)讨论文件类型判断时,我已经说过,在 Vim 里后缀不是判断文件类型的唯一依据。既然我懒到不愿意在汇编文件里加帮助识别的文本我当然也懒得去改汇编文件的后缀了。GCC 产生的汇编代码里的一些特定标识,也使得我利用文本判断变得相当容易:取决于不同的环境,汇编中一般会出现 `.arch arm``.cpu arm` 这样的明确行。
要让 Vim 进行文件类型判断,标准做法是在 ftdetect 目录下加入判断脚本。既然我们知道后缀是 .s在这个文件中我会写入
```
au BufRead *.[sS] call arm#ft#FTarm()
```
为了加快 Vim 的启动速度,真正检测需要的代码一般推荐放到 autoload 目录下。这是 Vim 的专门机制,允许脚本“按需”加载,仅在用到函数的时候,才载入函数的定义([`:help autoload`](https://yianwillis.github.io/vimcdoc/doc/eval.html#autoload))。在上面的写法下面,当 Vim 读入后缀为 .s 或 .S 的文件时Vim 会自动在 autoload/arm 目录下载入 ft.vim然后调用其中的 `FTarm` 函数。
下面我们来看一下 ft.vim 文件。这个文件不大,完整内容展示如下:
```
let s:cpo_save = &amp;cpo
set cpo&amp;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 &amp;cpo = s:cpo_save
unlet s:cpo_save
```
开头和结尾的四行属于 Vim 脚本的标准模板写法:进入脚本时保存兼容性选项([`:help 'cpoptions'`](https://yianwillis.github.io/vimcdoc/doc/options.html#'cpoptions'))的当前值,然后恢复其为默认值,免得其他地方的设置影响对脚本的解释;退出时则恢复原来保存的兼容性选项值。
中间主体部分就一个函数,做的事情也很简单,就是把文件的头五行内容拼到一起,然后看能不能找到满足条件的“.arch”和“.cpu”语句。找到的话就设置合适的文件类型找不到就不做处理留给其他的 Vim 脚本来继续判断。
这儿唯一比较特别点的地方是,一般设置文件类型推荐使用 `:setfiletype` 命令,它会避免重复设置,在一次 Vim 的自动事件触发过程中只执行一次。对于我们当前的目的,这是不够的:因为在我们的代码执行之前,当前缓冲区一般已经被系统的自动命令设置过类型了。具体来说,是运行支持文件里的 autoload/dist/ft.vim 里的 `dist#ft#FTasm` 函数。
所以,我们这儿需要强行覆盖已经设置的文件类型,用 `set filetype=…` 就可以做到。要注意,仅在你很有信心你设置的类型正确时才可以这么做,否则,你可能会干扰其他插件的结果。
这样,我就做到了在用 Vim 打开 GCC 产生的 ARM 汇编文件时,能自动检测并应用合适的 arm 语法。完整的代码可从 [adah1972/arm-syntax-vim](adah1972/arm-syntax-vim) 下载。
## 内容小结
今天我们分析了四个大小不同的 Vim 脚本,并展示了常见的 Vim 脚本用法。我们可以总结一下相关的知识点:
- Vim 里主要放置无条件执行脚本的目录是 plugin。
- `exists``expand``has` 恐怕是 Vim 里最重要的函数,常用法需要牢牢掌握。
- 好的 Vim 脚本一般会通过全局变量允许用户定制部分行为如键映射Vim 脚本里面通过 `exists` 函数来检测用户定义的全局变量。
- 一般来说,插件会使用自己的名字开始自己的自动命令组,这样比较便于管理,包括统一清除。
- `after` 目录下的内容会晚于其他目录加载。
- `ftdetect` 目录下一般放置用来检测文件类型的脚本。
- `autoload` 目录专门放置延迟加载的脚本。
## 课后练习
请选择一个你常用的插件(如 nerdtree 和 undotree分析它的主体结构看一下它使用了哪些不同的目录分成几个主要的模块提供了哪些命令又给用户留出了哪些定制点。
如果有任何的问题和想法,请留言和我交流。我们下一讲再见。

View File

@@ -0,0 +1,215 @@
<audio id="audio" title="拓展5 | 其他插件和技巧:吴咏炜的箱底私藏" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2c/f7/2cf64df52634a4e6b9f4f1a510bd75f7.mp3"></audio>
你好,我是吴咏炜。
上一讲我介绍了最重要的一些插件。今天这讲拓展,我们就算是查漏补缺,再分享一些我个人这些年压箱底的收藏。这些插件和技巧有新有旧,都非常好用,欢迎你挑选感兴趣的内容,纳入自己的个人收藏箱。
## 插件
### Syntastic 和 ALE
说到代码检查插件,我最早是从 [Syntastic](https://github.com/vim-syntastic/syntastic) 开始用的,然后慢慢转向了 [ALE](https://github.com/dense-analysis/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 次合并提交!
这两个插件的配置都略显复杂,通常需要你针对每种代码检查器进行配置。因此,总体来说,我对代码检查的推荐顺序是:
1. 使用 YCM、Python-mode、Vim-go 等有语言针对性的插件,如果你用的语言被支持,并且插件集成的代码检查功能够用的话
1. 使用 ALE如果你的语言和代码检查插件它能够支持的话
1. 使用 Syntastic如果其他选项不适用或者你需要的检查执行够快的话
### Renamer
在需要对文件进行批量更名时,我会使用 [qpkorr/vim-renamer](http://github.com/qpkorr/vim-renamer) 插件。它提供 `:Renamer` 命令,会打开当前目录下所有文件的列表。你随后就可以利用 Vim 强大的正则表达式和编辑功能来调整这些名字了。在调整完成后,执行 `:Ren` 命令即可。
<img src="https://static001.geekbang.org/resource/image/52/d4/5220906bc8fc3afb09aab05f27bcdfd4.png" alt="FigX5.1" title="Renamer 的界面(额外展开了一层目录)">
注意,图中的第二、三行提供了命令的说明,比如用 `&gt;` 来多展开一层目录,等等。你应该只做行内的修改,而不去删除行或调整行的顺序,否则可能引致意外的后果。使用 `&lt;C-Del&gt;` 可以删除当前行的文件,这算是一种例外情况。
在图中我只是手工修改了“log.conf”那行的文件名。更常见的情况是利用 Vim 的编辑功能做批量操作。比如,你需要把文件名变成小写,可以选中要修改的部分然后使用 `gu` 命令。又比如,如果你的文件里有大量的编号,你希望对编号进行增减的操作,也可以利用 Vim 的 `&lt;C-A&gt;``&lt;C-X&gt;` 来对编号进行加减操作。当然,这时你可能需要使用可视模式的列选择(`&lt;C-V&gt;`)。
你还有可能需要注意一下选项 `nrformats`,因为如果其中含有 `octal` 的话Vim 会把 `0` 打头的数字序列当成八进制来处理。还好,如果你按照我目前给出的方式来设置 vimrc 配置文件的话,缺省里面不含 `octal`,只会对 `0b``0x` 打头的数字做特别处理,这就不会跟普通的十进制数字编号有任何冲突了。
### Undowarning
Vim 里有跨会话撤销修改的功能,这当然是它的强大的特色功能。不过,有时候也许你会发现,不小心多按了几下 `u`,你就退回到打开文件之前的版本去了。我想,这很有可能不是你想要的行为吧?如果你,像我一样,希望能够无限制地进行编辑撤销,同时还想在退回打开文件的状态之前能有一个提醒,那 undowarning.vim 可能就是你想要的。
这个插件不支持用包管理器自动安装。你需要自行下载 [undowarning.vim](https://github.com/thoughtstream/Damian-Conway-s-Vim-Setup/blob/master/plugin/undowarnings.vim),并把它放到你的 Vim 配置目录的 plugin 子目录下。下图是一个运行中的示例:
<img src="https://static001.geekbang.org/resource/image/1d/49/1db7f1ccc892a8217a54819346435249.gif" alt="FigX5.2" title="Undowarning 的效果">
### Rainbow
代码中括号多了,有时候眼睛就有点看不过来,需要有个更好的颜色提示。因此,就有了很多彩虹效果的 Vim 插件。在这些插件中,我最喜欢的是 [frazrepo/vim-rainbow](https://github.com/frazrepo/vim-rainbow),它最妙的地方是,居然能把 C++ 代码中的尖括号也进行加亮,还能基本不会在出现小于、大于、流输入输出时进行错误的加亮。效果见下图:
<img src="https://static001.geekbang.org/resource/image/3e/d0/3e141f1c2d3883dd8384182d4bc72bd0.png" alt="FigX5.3" title="彩虹括号的效果">
效果默认不自动启用,可以用 `:RainbowToggle` 命令来切换,或用 `:RainbowLoad` 命令来加载。我觉得在括号多的时候按需启用挺好,推荐!
### Auto-pairs
代码中永远有着大量成双成对的符号,输入一个,就自动出来另一个,会是一个非常有用的功能。但这样的功能,也需要处理一些特殊情况,比如,如果程序员输入了一对符号 `()`,结果千万不能是 `())`。在很多现代的编辑器上,这已经是个标准功能了,但 Vim 一直没有类似的功能。
实际上Vim 里已经有插件 [jiangmiao/auto-pairs](https://github.com/jiangmiao/auto-pairs) 支持了这个功能,并解决了大部分边角情况。我觉得可以推荐给大家。
这个插件,要不要推荐我还是犹豫了一下的。我一开始对它相当满意,但后来我发现仍然有一些边角情况处理不好,使用它就会导致无法编辑出我需要的效果。所以我又把它卸载了。但再后来,我又觉得,毕竟瑕不掩瑜,而且有问题时把它禁用不就得了!
所以,它的配置项我也只需要提一个,就是禁用的键映射。这个键映射由全局变量 `g:AutoPairsShortcutToggle` 控制,默认值是 `&lt;M-p&gt;`。如果你在 Mac 上,这个键多半就不工作了,除非你只在 Mac 终端里使用 Vim并且在终端应用里配置了“将 Option 键用作 Meta 键”。对于大部分 Mac 用户,你需要进行类似下面的配置(因为 Mac 上按 Option-P 会产生“π”):
```
let g:AutoPairsShortcutToggle = 'π'
```
其他内容就请自行查看它的帮助文件了。
### Largefile
如果你经常打开很大的日志文件,那 Vim 的一些自动功能可能不仅帮不了什么忙,反而会拖慢你的编辑速度。有一个 Vim 插件能在文件较大时自动关闭事件处理、撤销、语法加亮等功能,用来换取更快的处理速度和更短的响应时间。这个插件就是 [vim-scripts/LargeFile](https://github.com/vim-scripts/LargeFile)。
这个插件的功能比较简单,唯一需要配置的就是多大算大。你只需要在 vimrc 配置文件中把你对大文件的阈值(以 MB 为单位)写到 `g:LargeFile` 变量里即可。比如,如果你认为超过 100MB 算大文件,那我们这样写就可以了:
```
let g:LargeFile = 100
```
### Markdown Preview
你如果像我一样常常写 Markdown 的话,你应该会喜欢 [Markdown Preview](https://github.com/iamcco/markdown-preview.nvim) 这个插件。Markdown 本来最适用的场景就是浏览器,纯文本的 Vim 只能编辑没有好的预览终究是很不足的。Markdown Preview 解决了这个问题,让你在编辑的同时,可以在浏览器里看到实际的渲染效果。更令我吃惊的是,这个预览是完全实时、同步的,无需存盘,而且预览页面随着光标在 Vim 里移动而跟着滚动,效果相当酷。你可以直接到 Markdown Preview 的主页上看一下官方的示意图,我就不在这里放动图了。
这个插件唯一需要特别注意的是,你不能直接把 iamcco/markdown-preview.nvim 放到你的包管理器里了事。原因是它里面包含了需要编译的前端组件,需要下载或编译才行。在它的主页上描述了在不同包管理器里的安装方式,你只要跟着照做就行。
它的配置在主页上也有列表,但默认设置就已经完全可用了。如果有需求的话,你可以修改其中部分值,如 `g:mkdp_browser` 可以用来设定你希望打开页面的浏览器(我目前设的是 `'firefox'`)。
### Calendar
[Calendar](https://github.com/mattn/calendar-vim) 是一个很简单的显示日历的 Vim 插件,在包管理器里的名字是 mattn/calendar-vim。它的功能应该就不需要解释了效果可以直接查看下图。
<img src="https://static001.geekbang.org/resource/image/b0/ea/b020f95be4c0ee37683b2178ef7ccbea.png" alt="FigX5.4" title=":CalendarH 产生的水平日历">
这个插件支持一些不同的样式和分割方式,上图中就是用 `:CalendarH`(或正常模式命令 `caL`)进行的横向分割,同时横向显示日历,你也可以用 `:Calendar`(或正常模式命令 `cal`)进行纵向分割和日历显示。此外,它还支持其他一些命令和组合。鉴于这个插件的的帮助文件也不长,如果你对相关功能有兴趣的话,就请你自己去看一下了。
### Matrix
上面介绍的插件,不管对你有没有用,都可以说是“有用”的。插件也不一定要做有用的事,我的机器一直装着下面这个“没用”的插件,[uguu-org/vim-matrix-screensaver](https://github.com/uguu-org/vim-matrix-screensaver)。
它的效果不用解释,直接看下面的动图就好。<br>
<img src="" alt=""><br>
<img src="https://static001.geekbang.org/resource/image/d9/7f/d96a5b5e193951fd8e3943yy3b97d67f.gif" alt="FigX5.5" title="Matrix!">
### Killersheep
Vim 脚本不仅可以做没用的事情还可以更进一步做娱乐的事情。比如Vim 的作者 Bram 亲自操刀写了这个“愚蠢的游戏”,[vim/killersheep](https://github.com/vim/killersheep)。
这当然不是一个真正非常好玩的游戏,不过我也玩通关了。你不妨也试试?小提示:屏幕拉高点,按键重复速度快点,重复前延迟短一点,这样更有助于你打好这个游戏。
当然,这个插件的本来意义就不是一个游戏,而是要演示 Vim 8.2 的下列新功能:
- 带颜色和掩码的的弹出窗口
- 用于高亮文本的文本属性
- 声音
如果你想用这些功能的话,就可以去看看这个插件的源码。这也算一种寓教于乐吧?
## 技巧
### 行过滤
在编辑日志等类型的文本时,我们往往想过滤出我们感兴趣的内容。这时,我们可以用正则表达式,但使用 `:s` 命令并不是一种最高效的方式。如果你感兴趣的每一行都可以跟某个正则表达式模式匹配(如日期、某关键字等),最高效的命令应该是:
```
:v/匹配模式/d
```
稍微解释一下,`:v` 命令(可以查看帮助 [`:help :v`](https://yianwillis.github.io/vimcdoc/doc/repeat.html#:v))可以用来找出不符合匹配模式的行(对比一下 `grep -v` 命令),然后执行后面的动作。所以上面的命令就是找出不满足匹配模式的行,然后执行删除(`d`)。
顺便说一下,如果你需要找出符合匹配模式的行,需要的命令是 `:g`(可以查看帮助 [`:help :g`](https://yianwillis.github.io/vimcdoc/doc/repeat.html#:g))。
### 自动关闭最后一个 quickfix 窗口
我们已经讨论到很多功能会用到 quickfix 窗口。如果打开 quickfix 窗口后,你关闭了编辑的主窗口,那 quickfix 窗口可能就成为这个 Vim 会话里剩下的唯一窗口了,而这个窗口多半你完全不再需要。你会希望,此时 Vim 应该自动关闭这个窗口直接退出。这当然是个可以用程序自动化的事情,所以我们也应该这样做,用下面的脚本放到 vimrc 配置文件里就可以做到:
```
aug QFClose
au!
au WinEnter * if winnr('$') == 1 &amp;&amp; &amp;buftype == "quickfix"|q|endif
aug END
```
你只要查一下 `winnr` 函数的帮助([`:help winnr()`](https://yianwillis.github.io/vimcdoc/doc/eval.html#winnr()))就很容易理解了,代码的意思还是非常清楚的:如果窗口的数量是 1 并且缓冲区类型是 quickfix 的话,那就退出 Vim。为了确保重复执行这段代码没有问题它有一个自己的自动命令组并会在清除这个自动命令组的所有自动命令后在进入窗口WinEnter这个事件中进行上面的检查。
(本技巧来自这个 [Stack Overflow 回答](https://stackoverflow.com/a/7477056/816999)。)
### Home 键的行为
对于大部分现代的编辑器Home 键的行为通常是:
- 当光标处于本行第一个非空白字符上时,跳转到行首
- 否则,跳转到本行第一个非空白字符上
虽然 Vim 的行为不是这样,但如果你希望配置出这样的行为,也不麻烦,把下面的代码加入到你的 vimrc 配置文件即可:
```
function! GoToFirstNonBlankOrFirstColumn()
let cur_col = col('.')
normal! ^
if cur_col != 1 &amp;&amp; cur_col == col('.')
normal! 0
endif
return ''
endfunction
nnoremap &lt;silent&gt; &lt;Home&gt; :call GoToFirstNonBlankOrFirstColumn()&lt;CR&gt;
inoremap &lt;silent&gt; &lt;Home&gt; &lt;C-R&gt;=GoToFirstNonBlankOrFirstColumn()&lt;CR&gt;
```
在这个代码里,`col('.')` 用来获取光标所在的列号,然后我们跳转到第一个非空白字符处(`^`),随后检查是不是我们不在第 1 列并且列号没有变化。如果是的话,说明第一个非空白字符不在行首并且当前光标已经在第一个非空白字符处了,那我们就跳转到行首去(`0`)。
另外要注意,我们这儿使用了 `normal!` 而不是 `normal`。这两者的区别是,用了 `!``normal` 命令会忽略键映射,否则 `normal` 就跟正常按键一样了。为了防止其他地方定义了键映射导致行为变化,一般推荐 `normal!` 命令而不是 `normal` 命令。
我们这样就修改了正常模式和插入模式下的 Home 键的行为。目前,在可视模式下这个方式不适用,你仍然只能手工选择合适的 `0``^` 命令。
### 查看光标下字符的内码
有很多字符是很相似的,在 Vim 使用的等宽字体中尤其如此。比如,光看字形,你能区分下面这些字符分别是什么吗?
- `- ─`
这些字符看起来虽然相似,但它们的意义是完全不同的。特别是在源代码中,如果你一不小心混入了一个相似的字符(字处理器有时候会自动替换一些 ASCII 字符,造成这种问题),代码运行就会出错了。这时,`ga` 命令就可以帮上忙。
下图展示了 `ga` 命令的一次执行结果:
<img src="https://static001.geekbang.org/resource/image/a1/74/a193f0d446ff7d5edcebfbc0e7494774.png" alt="FigX5.6" title="执行 ga 命令的结果">
我们可以看到这个字符实际上是 Unicode 字符 U+2013十进制和八进制数值分别是 8211 和 20023短破折号。这个字符在很多键盘上不能直接打出来因而 Vim 提供了二合字母digraph可以用 `&lt;C-K&gt;-N` 的方式来输入。
### 为什么是 42
好吧,最后这个不是技巧。你可能会奇怪:为什么在讲编程的很多代码里会出现 42 这个数字呢?那其实是因为一个科幻小说的“梗”。事实上,我在这个课程里也用到了 42。要想获取进一步的信息请在 Vim 里输入:
```
:help 42
```
## 内容小结
本讲的内容比较零散,我为你介绍了一些我认为值得介绍的插件和技巧,每一则篇幅都较短,我也就不再正式总结了。
我分享的这些内容大部分是有用的,但也有一部分可以认为基本是“无用”的。生活里如果要求每一件事情都有用,岂不是会变得非常无趣?对我来说,编程和编程工具也是如此。
## 课后练习
今天介绍的插件和技巧,虽说不能算必需,也是非常有意义的。强烈建议你挑至少三个来安装/实验一下。如有任何问题或意见,欢迎留言和我交流。
我是吴咏炜,我们下一讲再见!