Vim 的局部选项、quickfix 与 tags:设置作用在哪里,跳转依据什么
给 YAML 文件配了两格缩进,之后打开的每一个文本文件也都变成了两格。编译报错列表里,Python 脚本的错误一条也跳不过去。改了函数名之后,按
Ctrl-]跳转时 Vim 报「找不到标签模式」,重新生成标签文件,旧名字却还能搜到。
这三个问题分别涉及 Vim 的三个机制:选项的作用域(全局值与缓冲区局部值)、quickfix(把工具输出解析成可跳转的位置列表)、tags(从名称到定义位置的索引)。它们是把 Vim 用成开发环境的基础,也是语言服务器出现之后仍然在用的东西:语言服务器的诊断最后也进入 quickfix 或位置列表,很多项目在没有语言服务器时仍然靠 tags 跳转。
本文在 Vim 9.1 和 Universal Ctags 5.9 上逐个验证。实验见文末。
一、set 与 setlocal:选项的两份值
很多选项(shiftwidth、expandtab、textwidth 等)同时有一个全局值和每个缓冲区自己的局部值。新打开的缓冲区从全局值复制一份作为局部值。
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 对这类选项同时改局部值和全局值,之后打开的文件都会继承。开头那个问题就出在这里:
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 时写进文件的内容由后三个共同决定:
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 解析成条目:
默认 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。这两个命令调用的是外部程序,读的是磁盘上的文件,不是编辑器里的内容:
删掉第 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 返回。
ctags 为 total 生成的条目:/^int total(int n) { return price(n) + 5; }$/条目里记的默认不是行号,而是一个搜索模式:定义所在的整行。所以在文件里增删别的行,跳转仍然准确;改了定义所在的那一行,跳转就失败了:
在文件开头插入 3 行、不重建 tags::tag total 跳到第 5 行(按搜索模式找,行号变化不影响)
把 total 改名为 grand_total、不重建 tags::tag total 报 Vim(tag):E434: Can't find tag pattern更麻烦的是增量更新:
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维护标签文件;改名、删除、切换分支后重建。
配套实验
- codesphere-labs/engineering/vim-options-quickfix-tags:Vim 9.1、Universal Ctags 5.9:set 与 setlocal、ftplugin 里的 set、Tab 相关选项的组合、errorformat、:grep 与 :vimgrep、tags 的搜索模式与 ctags -a(验证记录)
参考资料
- Vim 帮助:
:help local-options、:help 'softtabstop'、:help quickfix、:help errorformat、:help location-list、:help tags-and-searches、:help editorconfig-install - Universal Ctags 文档:ctags(1)(--append、tag file format)
- Vim 官方文档(在线版)