2020年了,不要再看网上那些老旧的文章还在教你使用手工生成 tags 的,请使用自动代码索引生成工具,比如 vim-gutentags,现在网上好像就没有一篇能正确讨论 Vim C/C++ 环境搭建的,都在谈些十年前的东西,所以我写了篇关于 Vim 8 和 C/C++ 相关插件的介绍:
假设你已经有一定 Vim 使用经验,并且折腾过 Vim 配置,能够相对舒适的在 Vim 中编写其他代码的时候,准备在 Vim 开始 C/C++ 项目开发,或者你已经用 Vim 编写了几年 C/C++ 代码,想要更进一步,让自己的工作更加顺畅的话,本文就是为你准备的:
最简单的编译单个文件,和 sublime 的默认 build system 差不多,我们定义 F9 为编译单文件:
nnoremap <silent> <F9> :AsyncRun gcc -Wall -O2 "$(VIM_FILEPATH)" -o "$(VIM_FILEDIR)/$(VIM_FILENOEXT)" <cr>
其中 $(…) 形式的宏在执行时会被替换成实际的文件名或者文件目录,这样按 F9 就可以编译当前文件,同时按 F5 运行:
nnoremap <silent> <F5> :AsyncRun -raw -cwd=$(VIM_FILEDIR) "$(VIM_FILEDIR)/$(VIM_FILENOEXT)" <cr>
用双引号引起来避免文件名包含空格,“-cwd=$(VIM_FILEDIR)” 的意思时在文件文件的所在目录运行可执行,后面可执行使用了全路径,避免 linux 下面当前路径加 “./” 而 windows 不需要的跨平台问题。
参数 `-raw` 表示输出不用匹配错误检测模板 (errorformat) ,直接原始内容输出到 quickfix 窗口。这样你可以一边编辑一边 F9 编译,出错了可以在 quickfix 窗口中按回车直接跳转到错误的位置,编译正确就接着执行。
接下来是项目的编译,不管你直接使用 make 还是 cmake,都是对一群文件做点什么,都需要定位到文件所属项目的目录,AsyncRun 识别当前文件的项目目录方式和 gutentags相同,从文件所在目录向上递归,直到找到名为 “.git”, “.svn”, “.hg”或者 “.root”文件或者目录,如果递归到根目录还没找到,那么文件所在目录就被当作项目目录,你重新定义项目标志:
let g:asyncrun_rootmarks = ['.svn', '.git', '.root', '_darcs', 'build.xml']
然后在 AsyncRun 命令行中,用 “<root>” 或者 “$(VIM_ROOT)”来表示项目所在路径,于是我们可以定义按 F7 编译整个项目:
nnoremap <silent> <F7> :AsyncRun -cwd=<root> make <cr>
那么如果你有一个项目不在 svn 也不在 git 中怎么查找 <root> 呢?很简单,放一个空的 .root 文件到你的项目目录下就行了,前面配置过,识别名为 .root 的文件。
继续配置用 F8 运行当前项目:
nnoremap <silent> <F8> :AsyncRun -cwd=<root> -raw make run <cr>
当然,你的 makefile 中需要定义怎么 run ,接着按 F6 执行测试:
nnoremap <silent> <F6> :AsyncRun -cwd=<root> -raw make test <cr>
如果你使用了 cmake 的话,还可以照葫芦画瓢,定义 F4 为更新 Makefile 文件,如果不用 cmake 可以忽略:
nnoremap <silent> <F4> :AsyncRun -cwd=<root> cmake . <cr>
由于 C/C++ 标准库的实现方式是发现在后台运行时会缓存标准输出直到程序退出,你想实时看到 printf 输出的话需要 fflush(stdout) 一下,或者程序开头关闭缓存:“setbuf(stdout, NULL);” 即可。
同时,如果你开发 C++ 程序使用 std::cout 的话,后面直接加一个 std::endl 就强制刷新缓存了,不需要弄其他。而如果你在 Windows 下使用 GVim 的话,可以弹出新的 cmd.exe 窗口来运行刚才的程序:
nnoremap <silent> <F5> :AsyncRun -cwd=$(VIM_FILEDIR) -mode=4 "$(VIM_FILEDIR)/$(VIM_FILENOEXT)" <cr>nnoremap <silent> <F8> :AsyncRun -cwd=<root> -mode=4 make run <cr>
在 Windows 下使用 -mode=4 选项可以跟 Visual Studio 执行命令行工具一样,弹出一个新的 cmd.exe窗口来运行程序或者项目,于是我们有了下面的快捷键:
- F4:使用 cmake 生成 Makefile
- F5:单文件:运行
- F6:项目:测试
- F7:项目:编译
- F8:项目:运行
- F9:单文件:编译
- F10:打开/关闭底部的 quickfix 窗口
恩,编译和运行基本和 NotePad++ / GEdit 的体验差不多了。如果你重度使用 cmake 的话,你还可以写点小脚本,将 F4 和 F7 的功能合并,检测 CMakeLists.txt 文件改变的话先执行 cmake 更新一下 Makefile,然后再执行 make,否则直接执行 make,这样更自动化些。
动态检查
代码检查是个好东西,让你在编辑文字的同时就帮你把潜在错误标注出来,不用等到编译或者运行了才发现。我很奇怪 2018 年了,为啥网上还在到处介绍老旧的 syntastic,但凡见到介绍这个插件的文章基本都可以不看了。老的 syntastic 基本没法用,不能实时检查,一保存文件就运行检查器并且等待半天,所以请用实时 linting 工具 ALE:
即便你使用各类 C/C++ IDE,也只能给实时你标注一些编译错误或者警告,而使用 ALE + cppcheck/gcc,连上面类似的潜在错误都能帮你自动找出来,并且当你光标移动过去时在最下面命令行提示你错误原因。
用上一段时间以后,让你写 C/C++ 有一种安心和舒适的感觉。
修改比较
这是个小功能,在侧边栏显示一个修改状态,对比当前文本和 git/svn 仓库里的版本,在侧边栏显示修改情况,以前 Vim 做不到实时显示修改状态,如今推荐使用 vim-signify 来实时显示修改状态,它比 gitgutter 强,除了 git 外还支持 svn/mercurial/cvs 等十多种主流版本管理系统。
没注意到它时,你可能觉得它不存在,当你有时真的看上两眼时,你会发现这个功能很贴心。最新版 signify 还有一个命令`:SignifyDiff`,可以左右分屏对比提交前后记录,比你命令行 svn/git diff 半天直观多了。并且对我这种同时工作在 subversion 和 git 环境下的情况契合的比较好。
Signify 和前面的 ALE 都会在侧边栏显示一些标记,默认侧边栏会自动隐藏,有内容才会显示,不喜欢侧边栏时有时无的行为可设置强制显示侧边栏:“set signcolumn=yes” 。
文本对象
相信大家用 Vim 进行编辑时都很喜欢文本对象这个概念,diw 删除光标所在单词,ciw 改写单词,vip 选中段落等,ci”/ci( 改写引号/括号中的内容。而编写 C/C++ 代码时我推荐大家补充几个十分有用的文本对象,可以使用 textobj-user 全家桶:
Plug 'kana/vim-textobj-user'Plug 'kana/vim-textobj-indent'Plug 'kana/vim-textobj-syntax'Plug 'kana/vim-textobj-function', { 'for':['c', 'cpp', 'vim', 'java'] }Plug 'sgur/vim-textobj-parameter'
它新定义的文本对象主要有:
- i, 和 a, :参数对象,写代码一半在修改,现在可以用 di, 或 ci, 一次性删除/改写当前参数
- ii 和 ai :缩进对象,同一个缩进层次的代码,可以用 vii 选中,dii / cii 删除或改写
- if 和 af :函数对象,可以用 vif / dif / cif 来选中/删除/改写函数的内容
最开始我不太想用额外的文本对象,一直在坚持 Vim 固有的几个默认对象,生怕手练习惯了肌肉形成记忆到远端没有环境的 vim 下形成依赖改不过来,后来我慢慢发现挺有用的,比如改写参数,以前是比较麻烦的事情,这下流畅了很多,当我发现自己编码效率得到比较大的提升时,才发现习惯依赖不重要,行云流水才是真重要。以前看到过无数次都选择性忽略的东西,有时候试试可能会有新的发现。
编辑辅助
大家都知道 color 文件定义了众多不同语法元素的色彩,还有一个关键因素就是语法文件本身能否识别并标记得出众多不同的内容来?语法文件对某些东西没标注,你 color 文件确定了颜色也没用。因此 Vim 下面写 C/C++ 代码,语法高亮准确丰富的话能让你编码的心情好很多,这里推荐vim-cpp-enhanced-highlight 插件,提供比 Vim 自带语法文件更好的 C/C++ 语法标注,支持 标准 11/14/17。
前面编译运行时需要频繁的操作 quickfix 窗口,ale查错时也需要快速再错误间跳转(location list),就连文件比较也会用到快速跳转到上/下一个差异处,unimpaired 插件帮你定义了一系列方括号开头的快捷键,被称为官方 Vim 中丢失的快捷键。
我们好些地方用到了 quickfix / location 窗口,你在 quickfix 中回车选中一条错误的话,默认会把你当前窗口给切走,变成新文件,虽然按 CTRL+O 可以返回,但是如果不太喜欢这样切走当前文件的做法,可以设置 switchbuf,发现文件已在 Vim 中打开就跳过去,没打开过就新建窗口/标签打开,具体见帮助。
Vim最爽的地方是把所有 ALT 键映射全部留给用户了,尽量使用 Vim 的 ALT键映射,可以让冗长的快捷键缩短很多,请参考:《Vim和终端软件中支持ALT映射》。
代码补全
传统的 Vim 代码补全基本以 omni 系列补全和符号补全为主,omni 补全系统是 Vim 自带的针对不同文件类型编写不同的补全函数的基础语义补全系统,搭配 neocomplete 可以很方便的对所有补全结果(omni补全/符号补全/字典补全)进行一个合成并且自动弹出补全框,虽然赶不上 IDE 的补全,但是已经比大部分编辑器补全好用很多了。然而传统 Vim 补全还是有两个迈不过去的坎:语义补全太弱,其次是补全分析无法再后台运行,对大项目而言,某些复杂符号的补全会拖慢你的打字速度。
新一代的 Vim 补全系统,YouCompleteMe 和 Deoplete,都支持异步补全和基于 clang 的语义补全,前者集成度高,后者扩展方便。对于 C/C++ 的话,我推荐 YCM,因为 deoplete 的 clang 补全插件不够稳定,太吃内存,并且反应比较慢,它的代码量和代码质量和 YCM完全不是一个量级的。所以 C/C++ 的补全的话,请直接使用 YCM,没有之一,而使用 YCM的话,需要进行一些简单的调教:
let g:ycm_add_preview_to_completeopt = 0let g:ycm_show_diagnostics_ui = 0let g:ycm_server_log_level = 'info'let g:ycm_min_num_identifier_candidate_chars = 2let g:ycm_collect_identifiers_from_comments_and_strings = 1let g:ycm_complete_in_strings=1let g:ycm_key_invoke_completion = '<c-z>'set completeopt=menu,menuonenoremap <c-z> <NOP>let g:ycm_semantic_triggers = { 'c,cpp,python,java,go,erlang,perl': ['re!w{2}'], 'cs,lua,javascript': ['re!w{2}'], }
这样可以输入两个字符就自动弹出语义补全,不用等到输入句号 . 或者 -> 才触发,同时关闭了预览窗口和代码诊断这些 YCM 花边功能,保持清静,对于原型预览和诊断我们后面有更好的解决方法,YCM这两项功能干扰太大。
上面这几行配置具体每行的含义,可以见:《YouCompleteMe 中容易忽略的配置》。另外我在 Windows 下编译了一个版本,你用 Windows 的话无需下载VS编译,点击 [这里]。我日常开发使用 YCM 辅助编写 C/C++, Python 和 Go 代码,基本能提供 IDE 级别的补全。
函数列表
不再建议使用 tagbar, 它会在你保存文件的时候以同步等待的方式运行 ctags (即便你没有打开 tagbar),导致vim操作变卡,特别是 windows下开了反病毒软件扫描的话,有时候保存文件卡5-6秒。2018年了,我们有更好的选择,比如使用
@Yggdroot
开发的 LeaderF 来显示函数列表:
LeaderF 是目前匹配效率最高的,高过 CtrlP/Fzf 不少,敲更少的字母就能把文件找出来,同时搜索很迅速,使用 Python 后台线程进行搜索匹配,还有一个 C模块可以加速匹配性能,需要手工编译下。LeaderF在模糊匹配模式下按 TAB 可以切换到匹配结果窗口用光标或者 Vim 搜索命令进一步筛选,这是 CtrlP/Fzf 不具备的,更多方便的功能见它的官方文档。
文件/MRU 模糊匹配对于熟悉的项目效率是最高的,但对于一个新的项目,通常我们都不知道它有些什么文件,那就谈不上根据文件名匹配什么了,我们需要文件浏览功能。如果你喜欢把 Vim 伪装成 NotePad++ 之类的,那你该继续使用 NERDTree 进行文件浏览,但你想按照 Vim 的方式来,推荐阅读这篇文章:
Oil and vinegar – split windows and project drawer
然后像我一样开始使用 vim-dirvish,进行一些配置,比如当前文档按“-”号就能不切窗口的情况下在当前窗口直接返回当前文档所在的目录,再按一次减号就返回上一级目录,按回车进入下一级目录或者再当前窗口打开光标下的文件。进一步映射 “<tab>7” , “<tab>8” 和 “<tab>9” 分别用于在新的 split, vsplit 和新标签打开当前文件所在目录,这样从一个文件如手,很容易找到和该文件相关的其他项目文件。
最后一个是 C/C++ 的头文件/源文件快速切换功能,有现成的插件做这事情,比如 a.vim,我自己没用,因为这事情太简单,再我发现 a.vim 前我就觉得需要这个功能,然后自己两行 vim 脚本就搞定了。
它可以无缝的和前面的 YCM 搭配,用 libclang 给你生成参数提示,当你用 YCM 的 tab 补全了一个函数名后,只要输入左括号,下面命令行就会里面显示出该函数的参数信息,随着光标移动,下面还会高亮出来你正在处于哪个参数位置。
唯一需要设置的是使用 “set noshowmode”关闭模式提示,就是底部 —INSERT— 那个,我们一般都用 airline / lightline 之类的显示当前模式了,所以默认模式提示可以关闭,INSERT 模式下的命令行,完全留给 echodoc 显示参数使用。