4.7 声明/定义跳转
假设你正在分析某个开源项目源码,在 main.cpp 中遇到调用函数 func(),想要查看它如何实现,一种方式:在 main.cpp 中查找 -> 若没有在工程内查找 -> 找到后打开对应文件 -> 文件内查找其所在行 -> 移动光标到该行 -> 分析完后切换会先前文件,不仅效率太低更要命的是影响我的思维连续性。我需要另外高效的方式,就像真正函数调用一样:光标选中调用处的 func() -> 键入某个快捷键自动转换到 func() 实现处 -> 键入某个键又回到 func() 调用处,这就是所谓的定义跳转。
基本上,vim 世界存在两类导航:基于标签的跳转和基于语义的跳转。
基于标签的声明/定义跳转
继续延用前面接收标签系统的例子文件 main.cpp、my_class.h、my_class.cpp,第二步已经生成好了标签文件,那么要实现声明/定义跳转,需要第三步,引入标签文件。这让 vim 知晓标签文件的路径。在 /data/workplace/example/ 目录下用 vim 打开 main.cpp,在 vim 中执行如下目录引入标签文件 tags:
:set tags+=/data/workplace/example/tags
既然 vim 有个专门的命令来引入标签,说明 vim 能识别标签。虽然标签文件中并无行号,但已经有标签所在文件,以及标签所在行的完整内容,vim 只需切换至对应文件,再在文件内作内容查找即可找到对应行。换言之,只要有对应的标签文件,vim 就能根据标签跳转至标签定义处。
这时,你可以体验下初级的声明/定义跳转功能。把光标移到 main.cpp 的 one.printMsg() 那行的 printMsg 上,键入快捷键 g],vim 将罗列出名为 printMsg 的所有标签候选列表,按需选择键入编号即可跳转进入。如下图:
目前为止,离我预期还有差距。
第一,选择候选列表影响思维连续性。首先得明白为何会出现待选列表。前面说过,vim 做的事情很简单,就是把光标所在单词放到标签文件中查找,如果只有一个,当然你可以直接跳转过去,大部分时候会找到多项匹配标签,比如,函数声明、函数定义、函数调用、函数重载等等都会导致同个函数名出现在多个标签中,vim 无法知道你要查看哪项,只能让你自己选择。其实,因为标签文件中已经包含了函数签名属性,vim 的查找机制如果不是基于关键字,而是基于语义的话,那也可以直接命中,期待后续 vim 有此功能吧。既然无法直接解决,换个思路,我不想选择列表,但可以接受遍历匹配标签。就是说,我不想输入数字选择第几项,但可以接受键入正向快捷键后遍历第一个匹配标签,再次键入快捷键遍历第二个,直到最后一个,键入反向快捷键逆序遍历。这下事情简单了,命令 :tnext 和 :tprevious 分别先后和向前遍历匹配标签,定义两个快捷键搞定:
" 正向遍历同名标签
nmap <Leader>tn :tnext<CR>
" 反向遍历同名标签
nmap <Leader>tp :tprevious<CR>
等等,这还不行,vim 中有个叫标签栈(tags stack)的机制,:tnext、:tprevious 只能遍历已经压入标签栈内的标签,所以,你在遍历前需要通过快捷键 ctrl-] 将光标所在单词匹配的所有标签压入标签栈中,然后才能遍历。不说复杂了,以后你只需先键入 ctrl-],若没跳转至需要的标签,再键入 tn 往后或者 tp 往前遍历即可。如下图所示:
第二,如何返回先前位置。当分析完函数实现后,我需要返回先前调用处,可以键入 vim 快捷键 ctrl-t 返回,如果想再次进入,可以用前面介绍的方式,或者键入 ctrl-i。另外,注意,ctrl-o 以是一种返回快捷键,但与 ctrl-t 的返回不同,前者是返回上次光标停留行、后者返回上个标签。
第三,如何自动生成标签并引入。开发时代码不停在变更,每次还要手动执行 ctags 命令生成新的标签文件,太麻烦了,得想个法周期性针对这个工程自动生成标签文件,并通知 vim 引人该标签文件,嘿,还真有这样的插件 —— indexer(https://github.com/vim-scripts/indexer.tar.gz )。indexer 依赖 DfrankUtil(https://github.com/vim-scripts/DfrankUtil )、vimprj(https://github.com/vim-scripts/vimprj )两个插件,请一并安装。请在 .vimrc 中增加:
" 设置插件 indexer 调用 ctags 的参数
" 默认 --c++-kinds=+p+l,重新设置为 --c++-kinds=+p+l+x+c+d+e+f+g+m+n+s+t+u+v
" 默认 --fields=+iaS 不满足 YCM 要求,需改为 --fields=+iaSl
let g:indexer_ctagsCommandLineOptions="--c++-kinds=+p+l+x+c+d+e+f+g+m+n+s+t+u+v --fields=+iaSl --extra=+q"
另外,indexer 还有个自己的配置文件,用于设定各个工程的根目录路径,配置文件位于 ~/.indexer_files,内容可以设定为:
--------------- ~/.indexer_files ---------------
[foo]
/data/workplace/foo/src/
[bar]
/data/workplace/bar/src/
上例设定了两个工程的根目录,方括号内是对应工程名,路径为工程的代码目录,不要包含构建目录、文档目录,以避免将产生非代码文件的标签信息。这样,从以上目录打开任何代码文件时,indexer 便对整个目录创建标签文件,若代码文件有更新,那么在文件保存时,indexer 将自动调用 ctags 更新标签文件,indexer 生成的标签文件以工程名命名,位于 ~/.indexer_files_tags/,并自动引入进 vim 中,那么
:set tags+=/data/workplace/example/tags
一步也省了。好了,解决了这三个问题后,vim 的代码导航基本已经达到我的预期。
基于语义的声明/定义跳转
有个 vim 插件叫 YCM,有个 C++ 编译器叫 clang,只要正确使用它俩,你将获得无与伦比的代码导航用户体验,以及,代码补全。当然,代价是相对复杂的配置,涉及几个后续章节知识点(“基于语义的智能补全”和“编译器/构建工具集成”),正因如此,此时我只给出快捷键设置,在看完“基于语义的智能补全”后请返回此处,重新查阅。
请增加如下快捷键到 .vimrc 中:
nnoremap <leader>jc :YcmCompleter GoToDeclaration<CR>
" 只能是 #include 或已打开的文件
nnoremap <leader>jd :YcmCompleter GoToDefinition<CR>
效果如下:
另外,基于标签的调整建议你也要了解,有助于你熟悉标签系统,毕竟,使用标签的插件很有几个。