Skip to content

Vim 的局部选项、quickfix 与 tags:设置作用在哪里,跳转依据什么 ​

给 YAML 文件配了两格缩进,之后打开的每一个文本文件也都变成了两格。编译报错列表里,Python 脚本的错误一条也跳不过去。改了函数名之后,按 Ctrl-] 跳转时 Vim 报「找不到标签模式」,重新生成标签文件,旧名字却还能搜到。

这三个问题分别涉及 Vim 的三个机制:选项的作用域(全局值与缓冲区局部值)、quickfix(把工具输出解析成可跳转的位置列表)、tags(从名称到定义位置的索引)。它们是把 Vim 用成开发环境的基础,也是语言服务器出现之后仍然在用的东西:语言服务器的诊断最后也进入 quickfix 或位置列表,很多项目在没有语言服务器时仍然靠 tags 跳转。

本文在 Vim 9.1 和 Universal Ctags 5.9 上逐个验证。实验见文末。

选项作用在哪里quickfixtagsset sw=2当前缓冲区 + 全局值之后打开的 b.txt:2setlocal sw=2只有当前缓冲区之后打开的 b.txt:8ftplugin 里写 set:之后打开的每个文件都被改了file:12:5: error ...默认 errorformat有效条目,可以跳转ERROR [f line 7] ...不匹配无效条目,只有文字:grep、:make 读磁盘:vimgrep 读缓冲区(含未保存修改)total shop.c/^int total(int n) ...$/挪动行:仍跳到正确位置改名:E434 找不到模式ctags -a:旧条目仍在改名、删除、切换分支后重建语言服务器的诊断也进入 quickfix 或位置列表;tags 只按名字索引,不理解作用域与宏
图 1 · 实测(Vim 9.1、Universal Ctags 5.9):set 同时改局部值和全局值,之后打开的文件都继承,ftplugin 里必须用 setlocal;errorformat 匹配不上的行进入 quickfix 但不能跳转,:grep 和 :make 读磁盘、:vimgrep 读缓冲区;tags 用整行作搜索模式,挪行不影响,改名后找不到,ctags -a 不删旧条目。

一、set 与 setlocal:选项的两份值 ​

很多选项(shiftwidth、expandtab、textwidth 等)同时有一个全局值和每个缓冲区自己的局部值。新打开的缓冲区从全局值复制一份作为局部值。

text
a.txt 里 setlocal sw=2 后打开 b.txt:b.txt 的 shiftwidth=8
a.txt 里 set sw=2 后打开 b.txt:b.txt 的 shiftwidth=2(set 同时改了全局值)

:setlocal 只改当前缓冲区的局部值;:set 对这类选项同时改局部值和全局值,之后打开的文件都会继承。开头那个问题就出在这里:

text
ftplugin/yaml.vim 里写 set sw=2:c.yaml 的 shiftwidth=2,之后打开的 b.txt 也是 2

文件类型插件(ftplugin)在打开某种类型的文件时执行,里面的设置应当只影响这个文件,所以必须用 setlocal。Vim 自带的 ftplugin 都遵守这一点;自己写的 ftplugin、在 autocmd FileType 里写的设置,常常漏掉。查一个选项最后是谁设置的,用 :verbose setlocal shiftwidth?。

选项的作用域不止这两种:有的只有全局值(ignorecase),有的是窗口局部(wrap、number),还有「全局-局部」选项(makeprg、errorformat、tags),局部值为空时使用全局值。每个选项属于哪一类,看 :help 里选项名下面的那一行(global、local to buffer、local to window、global or local to buffer)。ftplugin 的加载顺序见Vim 的配置与插件怎样加载。

二、Tab 相关的四个选项 ​

缩进出问题时,最常见的困惑来自四个选项各管一件事:

选项管什么
tabstop文件里的一个制表符显示成几列
shiftwidth>>、<<、自动缩进一次移动几列
softtabstop插入模式下按一次 Tab 或退格移动几列
expandtab插入时用空格代替制表符

按 Tab 时写进文件的内容由后三个共同决定:

text
set expandtab shiftwidth=4 softtabstop=4,按 1 次 Tab 再输入 x:文件内容 ····x
set expandtab shiftwidth=4 softtabstop=4,按 2 次 Tab 再输入 x:文件内容 ········x
set noexpandtab tabstop=8 softtabstop=4 shiftwidth=4,按 1 次 Tab 再输入 x:文件内容 ····x
set noexpandtab tabstop=8 softtabstop=4 shiftwidth=4,按 2 次 Tab 再输入 x:文件内容 →x

(· 为空格,→ 为制表符。)第四行值得注意:noexpandtab 加上 softtabstop=4 时,按一次 Tab 写入 4 个空格,按两次时 Vim 把 8 个空格换成了一个制表符。文件里混着空格和制表符,换一个 tabstop 不同的编辑器打开就会错位。

实践中的选择很简单:要么 expandtab 加上相同的 shiftwidth 与 softtabstop(只用空格),要么 noexpandtab 且 shiftwidth、softtabstop、tabstop 三者相等(只用制表符,例如 Makefile 和 Go);tabstop 保持 8,不要用它来调缩进宽度。项目级的约定最好写在 .editorconfig 里,Vim 9.1 自带 EditorConfig 插件(:packadd editorconfig)。

三、quickfix:把工具输出变成位置列表 ​

quickfix 列表是 Vim 里所有「一组位置」的统一载体:编译错误、搜索结果、lint 诊断都可以放进去,用 :cnext、:cprev 依次跳转,:copen 打开列表窗口。外部工具的输出通过 errorformat 解析成条目:

text
默认 errorformat 解析三行日志:有效 src/order.c:12 error: 'total' undeclared|有效 my dir/pay.c:3 warning: unused variable 'x'|无效 :0 ERROR [src/report.py line 7] name 'rows' is not defined
加上 errorformat 'ERROR [%f line %l] %m':3 条有效;第三条:src/report.py:7 name 'rows' is not defined

默认的 errorformat 能识别 文件:行:列: 消息 这类 GCC 风格的输出(路径里有空格也可以),自定义格式的日志被当成无效条目:能在列表里看到文字,但不能跳转。为自己的工具加一条格式,用 set errorformat^=...(^= 表示加在最前面,优先匹配),%f、%l、%c、%m 分别是文件、行、列、消息。Vim 自带了很多编译器的格式,:compiler 命令可以一次设好 makeprg 和 errorformat。

:make 运行 makeprg 并把输出放进 quickfix,:grep 运行 grepprg。这两个命令调用的是外部程序,读的是磁盘上的文件,不是编辑器里的内容:

text
删掉第 2 行但不保存::grep(外部 grep 读磁盘)找到 1 处,:vimgrep(读缓冲区)找到 0 处

缓冲区里已经改掉的问题,:make 和 :grep 还会报出来;反过来,刚写的错误在保存之前也不会出现。运行外部工具之前先 :wall。:vimgrep 用 Vim 自己的正则在已加载的缓冲区内容上搜索,结果与编辑器里看到的一致,但大项目里比外部工具慢得多。Vim 的正则与 grep 的正则也不是同一种方言(见同一个正则在三种语言里结果不同)。

除了全局唯一的 quickfix 列表,每个窗口还可以有自己的位置列表(location list,命令以 l 开头::lgrep、:lnext)。插件和语言服务器客户端通常把当前文件的诊断放进位置列表,项目级的结果放进 quickfix,互不覆盖。

四、tags:名称到位置的索引 ​

ctags 扫描源码,为每个定义生成一条「名称、文件、定位方式」的记录。Vim 用 :tag 名称 或 Ctrl-] 跳转,Ctrl-t 返回。

text
ctags 为 total 生成的条目:/^int total(int n) { return price(n) + 5; }$/

条目里记的默认不是行号,而是一个搜索模式:定义所在的整行。所以在文件里增删别的行,跳转仍然准确;改了定义所在的那一行,跳转就失败了:

text
在文件开头插入 3 行、不重建 tags::tag total 跳到第 5 行(按搜索模式找,行号变化不影响)
把 total 改名为 grand_total、不重建 tags::tag total 报 Vim(tag):E434: Can't find tag pattern

更麻烦的是增量更新:

text
ctags -a 追加后:tags 里仍有 total 的条目 1 条,grand_total 1 条
重新生成 tags:total 0 条,grand_total 1 条

ctags -a 只追加新条目,不删除已经不存在的旧条目,标签文件会越来越「脏」:补全里出现早已删除的函数,跳转时报找不到。按文件增量更新需要先删掉该文件的旧条目再追加,这正是 gutentags 一类插件做的事;没有插件时,在改名、删除或切换分支后整体重建是最稳妥的做法。

tags 是纯文本层面的索引:它不知道作用域、不区分重载、不理解宏展开后的代码。同名的函数在不同模块里都有定义时,:tselect 会列出所有候选让你选。需要「这个调用实际调用的是哪个函数」时,要靠理解编译上下文的语言服务器,见语言服务器看到的编译上下文。

五、检查清单 ​

  • ftplugin 与 autocmd FileType 里一律用 setlocal;用 :verbose setlocal 选项? 查是谁设置的。
  • 缩进只用空格或只用制表符;tabstop 保持 8,项目约定写进 .editorconfig。
  • 自定义工具的输出配好 errorformat,确认条目是「有效」的。
  • 运行 :make、:grep 之前保存所有文件。
  • 不要依赖 ctags -a 维护标签文件;改名、删除、切换分支后重建。

配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权