5 代码开发
在具体编码过程中,我需要一系列提高生产力的功能:批量开/关注释、快速输入代码模板、代码智能补全、路径智能补全、从接口生成实现、查看参考库信息等等,我们逐一来实现。
5.1 快速开关注释
需要注释时,到每行代码前输入 //,取消注释时再删除 //,这种方式不是现代人的行为。IDE 应该支持对选中文本块批量(每行)添加注释符号,反之,可批量取消。本来 vim 通过宏方式可以支持该功能,但每次注释时要自己录制宏,关闭 vim 后宏无法保存,所以有人专门编写了一款插件 NERD Commenter(https://github.com/scrooloose/nerdcommenter ),NERD Commenter 根据编辑文档的扩展名自适应采用何种注释风格,如,文档名 x.cpp 则采用 // 注释风格,而 x.c 则是 /**/ 注释风格;另外,如果选中的代码并非整行,那么该插件将用 /**/ 只注释选中部分。
常用操作:
- \
cc,注释当前选中文本,如果选中的是整行则在每行首添加 //,如果选中一行的部分内容则在选中部分前后添加分别 /、/; - \
cu,取消选中文本块的注释。
如下图所示:
(快速开/关注释)
另外,有时需要 ASCII art 风格的注释,可用 DrawIt!(https://github.com/vim-scripts/DrawIt ),它可以让你用方向键快速绘制出。
常用操作就两个,:Distart,开始绘制,可用方向键绘制线条,空格键绘制或擦除字符;:Distop,停止绘制。
如下图所示:
(ASCII art 风格注释)
5.2 模板补全
开发时,我经常要输入相同的代码片断,比如 if-else、switch 语句,如果每个字符全由手工键入,我可吃不了这个苦,我想要简单的键入就能自动帮我完成代码模板的输入,并且光标停留在需要我编辑的位置,比如键入 if,vim 自动完成
if (/* condition */) {
TODO
}
而且帮我选中 / condition / 部分,不会影响编码连续性 —— UltiSnips(https://github.com/SirVer/ultisnips ),我的选择。
在进行模板补全时,你是先键入模板名(如,if),接着键入补全快捷键(默认 \
UltiSnips 有一套自己的代码模板语法规则,类似:
snippet if "if statement" i
if (${1:/* condition */}) {
${2:TODO}
}
endsnippet
其中,snippet 和 endsnippet 用于表示模板的开始和结束;if 是模板名;"if statement" 是模板描述,你可以把多个模板的模板名定义成一样(如,if () {} 和 if () {} else {} 两模板都定义成相同模板名 if),在模板描述中加以区分(如,分别对应 "if statement" 和 "if-else statement"),这样,在 YCM(重量级智能补全插件) 的补全列表中可以根据模板描述区分选项不同模板;i 是模板控制参数,用于控制模板补全行为,具体参见“快速编辑结对符”一节;${1}、${2} 是 \
新版 UltiSnips 并未自带预定义的代码模板,你可以从 https://github.com/honza/vim-snippets 获取各类语言丰富的代码模板,也可以重新写一套符合自己编码风格的模板。无论哪种方式,你需要在 .vimrc 中设定该模板所在目录名,以便 UltiSnips 寻找到。比如,我自定义的代码模板文件 cpp.snippets,路径为 ~/.vim/bundle/ultisnips/mysnippets/cpp.snippets,对应设置如下: let g:UltiSnipsSnippetDirectories=["mysnippets"] 其中,目录名切勿取为 snippets,这是 UltiSnips 内部保留关键字;另外,目录一定要是 ~/.vim/bundle/ 下的子目录,也就是 vim 的运行时目录。
完整 cpp.snippets 内容如下:
#=================================
#预处理
#=================================
# #include "..."
snippet INC
#include "${1:TODO}"${2}
endsnippet
# #include <...>
snippet inc
#include <${1:TODO}>${2}
endsnippet
#=================================
#结构语句
#=================================
# if
snippet if
if (${1:/* condition */}) {
${2:TODO}
}
endsnippet
# else if
snippet ei
else if (${1:/* condition */}) {
${2:TODO}
}
endsnippet
# else
snippet el
else {
${1:TODO}
}
endsnippet
# return
snippet re
return(${1:/* condition */});
endsnippet
# Do While Loop
snippet do
do {
${2:TODO}
} while (${1:/* condition */});
endsnippet
# While Loop
snippet wh
while (${1:/* condition */}) {
${2:TODO}
}
endsnippet
# switch
snippet sw
switch (${1:/* condition */}) {
case ${2:c}: {
}
break;
default: {
}
break;
}
endsnippet
# 通过迭代器遍历容器(可读写)
snippet for
for (auto ${2:iter} = ${1:c}.begin(); ${3:$2} != $1.end(); ${4:++iter}) {
${5:TODO}
}
endsnippet
# 通过迭代器遍历容器(只读)
snippet cfor
for (auto ${2:citer} = ${1:c}.cbegin(); ${3:$2} != $1.cend(); ${4:++citer}) {
${5:TODO}
}
endsnippet
# 通过下标遍历容器
snippet For
for (decltype($1.size()) ${2:i} = 0; $2 != ${1}.size(); ${3:++}$2) {
${4:TODO}
}
endsnippet
# C++11风格for循环遍历(可读写)
snippet F
for (auto& e : ${1:c}) {
}
endsnippet
# C++11风格for循环遍历(只读)
snippet CF
for (const auto& e : ${1:c}) {
}
endsnippet
# For Loop
snippet FOR
for (unsigned ${2:i} = 0; $2 < ${1:count}; ${3:++}$2) {
${4:TODO}
}
endsnippet
# try-catch
snippet try
try {
} catch (${1:/* condition */}) {
}
endsnippet
snippet ca
catch (${1:/* condition */}) {
}
endsnippet
snippet throw
th (${1:/* condition */});
endsnippet
#=================================
#容器
#=================================
# std::vector
snippet vec
vector<${1:char}> v${2};
endsnippet
# std::list
snippet lst
list<${1:char}> l${2};
endsnippet
# std::set
snippet set
set<${1:key}> s${2};
endsnippet
# std::map
snippet map
map<${1:key}, ${2:value}> m${3};
endsnippet
#=================================
#语言扩展
#=================================
# Class
snippet cl
class ${1:`Filename('$1_t', 'name')`}
{
public:
$1 ();
virtual ~$1 ();
private:
};
endsnippet
#=================================
#结对符
#=================================
# 括号 bracket
snippet b "bracket" i
(${1})${2}
endsnippet
# 方括号 square bracket,设定为 st 而非 sb,避免与 b 冲突
snippet st "square bracket" i
[${1}]${2}
endsnippet
# 大括号 brace
snippet br "brace" i
{
${1}
}${2}
endsnippet
# 单引号 single quote,设定为 se 而非 sq,避免与 q 冲突
snippet se "single quote" I
'${1}'${2}
endsnippet
# 双引号 quote
snippet q "quote" I
"${1}"${2}
endsnippet
# 指针符号 arrow
snippet ar "arrow" i
->${1}
endsnippet
# dot
snippet d "dot" i
.${1}
endsnippet
# 作用域 scope
snippet s "scope" i
::${1}
endsnippet
默认情况下,UltiSnips 模板补全快捷键是 \
" UltiSnips 的 tab 键与 YCM 冲突,重新设定
let g:UltiSnipsExpandTrigger="<leader><tab>"
let g:UltiSnipsJumpForwardTrigger="<leader><tab>"
let g:UltiSnipsJumpBackwardTrigger="<leader><s-tab>"
效果如下:
(模板补全)
5.3 智能补全
真的,这绝对是 G 点。智能补全是提升编码效率的杀手锏。试想下,有个函数叫 getCountAndSizeFromRemotefile(),当你输入 get 后 IDE 自动帮你输入完整的函数名,又如,有个文件 ~/this/is/a/deep/dir/file.txt,就像在 shell 中一样,键入 tab 键自动补全文件路径那是何等惬意!
智能补全有两类实现方式:基于标签的、基于语义的。
基于标签的智能补全
前面代码导航时介绍过标签,每个标签项含有标签名、作用域等等信息,当键入某几个字符时,基于标签的补全插件就在标签文件中搜索匹配的标签项,并罗列出来,你选择中意的,这与前面代码导航类似,一个是用于跳转、一个用于输入。基于标签的补全,后端 ctags 先生成标签文件,前端采用插件 new-omni-completion(内置)进行识别。这种方式操作简单、效果不错,一般来说两步搞定。
第一步,生成标签文件。在工程目录的根目录执行 ctags,该目录下会多出个 tags 文件;
第二步,引入标签文件。在 vim 中引入标签文件,在 vim 中执行命令
:set tags+=/home/your_proj/tags
后续,在编码时,键入标签的前几个字符后依次键入 ctrl-x ctrl-o 将罗列匹配标签列表、若依次键入 ctrl-x ctrl-i 则文件名补全、ctrl-x ctrl-f 则路径补全。
举个例子,演示如何智能补全 C++ 标准库。与前面介绍的一般步骤一样,先调用 ctags 生成标准库的标签文件,再在 vim 中引入即可,最后编码时由相应插件实时搜索标签文件中的类或模板,显示匹配项:
首先,获取 C++ 标准库源码文件。安装的 GNU C++ 标准库源码文件,openSUSE 可用如下命令:
zypper install libstdc++48-devel
安装成功后,在 /usr/include/c++/4.8/ 可见到所有源码文件;
接着,执行 ctags 生成标准库的标签文件:
cd /usr/include/c++/4.8
ctags -R --c++-kinds=+l+x+p --fields=+iaSl --extra=+q --language-force=c++ -f stdcpp.tags
然后,让 OmniCppComplete 成功识别标签文件中的标准库接口。C++ 标准库源码文件中使用了 _GLIBCXX_STD 名字空间(GNU C++ 标准库的实现是这样,如果你使用其他版本的标准库,需要自行查找对应的名字空间名称),标签文件里面的各个标签都嵌套在该名字空间下,所以,要让 OmniCppComplete 正确识别这些标签,必须显式告知 OmniCppComplete 相应的名字空间名称。在.vimrc中增加如下内容:
let OmniCpp_DefaultNamespaces = ["_GLIBCXX_STD"]
最后,在 vim 中引入该标签文件。在 .vimrc 中增加如下内容:
set tags+=/usr/include/c++/4.8/stdcpp.tags
后续你就可以进行 C++ 标准库的代码补全,比如,在某个 string 对象名输入 . 时,vim 自动显示成员列表。如下图所示:
(基于标签的 C++ 标准库补全)
没明白? -。-# 咱再来个例子,看看如何补全 linux 系统 API。与前面的标准库补全类似,唯一需要注意,linux 系统 API 头文件中使用了 GCC 编译器扩展语法,必须告诉 ctags 在生成标签时忽略之,否则将生产错误的标签索引。
首先,获取 linux 系统 API 头文件。openSUSE 可用如下命令:
zypper install linux-glibc-devel
安装成功后,在 /usr/include/ 中可见相关头文件;
接着,执行 ctags 生成系统 API 的标签文件。linux 内核采用 GCC 编译,为提高内核运行效率,linux 源码文件中大量采用 GCC 扩展语法,这影响 ctags 生成正确的标签,必须借由 ctags 的 -I 命令参数告之忽略某些标签,若有多个忽略字符串之间用逗号分割。比如,在文件 unistd.h 中几乎每个API声明中都会出现 THROW、nonnull 关键字,前者目的是告诉 GCC 这些函数不会抛异常,尽量多、尽量深地优化这些函数,后者目的告诉 GCC 凡是发现调用这些函数时第一个实参为 nullptr 指针则将其视为语法错误,的确,使用这些扩展语法方便了我们编码,但却影响了 ctags 正常解析,这时可用 -I THROW,nonnull 命令行参数让 ctags 忽略这些语法扩展关键字:
cd /usr/include/
ctags -R --c-kinds=+l+x+p --fields=+lS -I __THROW,__nonnull -f sys.tags
最后,在 vim 中引入该标签文件。在 .vimrc 中增加如下内容:
set tags+=/usr/include/sys.tags
从以上两个例子来看,不论是 C++ 标准库、boost、ACE这些重量级开发库,还是 linux 系统 API 均可遵循“下载源码(至少包括头文件)-执行 ctags 生产标签文件-引入标签文件”的流程实现基于标签的智能补全,若有异常,唯有如下两种可能:一是源码中使用了名字空间,借助 OmniCppComplete 插件的 OmniCppDefaultNamespaces 配置项解决;一是源码中使用了编译器扩展语法,借助 ctags 的 -I 参数解决(上例仅列举了少量 GCC 扩展语法,此外还有 _attribute_malloc、__wur 等等大量扩展语法,具体请参见 GCC 手册。以后,如果发现某个系统函数无法自动补全,十有八九是头文件中使用使用了扩展语法,先找到该函数完整声明,再将其使用的扩展语法加入 -I 列表中,最后运行 ctags 重新生产新标签文件即可)。
基于语义的智能补全
对于智能补全只有轻度需求的用户来说,基于标签的补全能够很好地满足需求,但对于我这类重度需求用户来说,但凡涉及标签,就存在以下几个问题:
- 问题一,必须定期调用 ctags 生成标签文件。代码在不停更新,每次智能补全前你得先手动生成标签文件,这还好,你可以借助前面的 indexer 在保存文件时更新标签文件;麻烦的是,你代码中要用到 C++ 标准库中的接口吧,那么事前你先得对整个标准库生成标签文件,这也还好,无非多个步骤;更昏人的是,你得使用了编译器语法扩展的库,你还得一条条找出具体使用了哪些扩展语言,再让 ctags 忽略执行语法关键字,真有够麻烦的;
- 问题二,ctags 本身对 C++ 支持有限。面对函数形参、重载操作符、括号初始化的对象,ctags 有心无力;对于 C++11 新增 lambda 表达式、auto 类型推导更是不认识。
我需要更优的补全机制 —— 基于语义的智能补全。语义补全,实时探测你是否有补全需求,无须你定期生成标签,可解决问题一;语义补全,是借助编译器进行代码分析,只要编译器对 C++ 规范支持度高,不论标准库、类型推导,还是 boost 库中的智能指针都能补全。什么是语义分析补全?看下图:
(语义分析补全)
(YCM 的语义补全)
(YCM 的标签补全)
YCM 的 OmniCppComplete 补全引擎。我要进行 linux 系统开发,打开系统函数头文件觉得麻烦(也就无法使用 YCM 的语义补全),引入系统函数 tag 文件又影响 vim 速度(也就无法使用 YCM 的标签补全),这种情况又如何让 YCM 补全呢?WOW,别担心,YCM 还有 OmniCppComplete 补全引擎,只要你在当前代码文件中 #include 了该标识符所在头文件即可。通过 OmniCppComplete 补全无法使用 YCM 的随键而全的特性,你需要手工告知 YCM 需要补全,OmniCppComplete 的默认补全快捷键为 \
inoremap <leader>; <C-x><C-o>
比如,我要补全 fork(),该函数所在头文件为 unistd.h,正确添加 #include \
(YCM 的 OmniCppComplete 补全引擎)
(OmniCppComplete 替代语义、标签补全)
YCM 的其他补全。YCM 还集成了其他辅助补全引擎,可以补全路径、头文件、甚至可以在注释中继续补全。如下图:
(YCM 的其他补全)
从我的经验来看,要想获得最好的补全体验,你应综合使用 YCM 的各种补全引擎!
另外,YCM 不愧是 google 工程师开发的,它的匹配项搜索方式非常智能,你无须从前往后逐一输入,YCM 会对你输入的内容进行模糊搜索,如下:
(YCM 模糊搜索)
(YCM 大小写智能敏感)
5.4 由接口快速生成实现框架
在 \*.h 中写成员函数的声明,在 \*.cpp 中写成员函数的定义,很麻烦,我希望能根据类声明自动生成类实现的代码框架 —— vim-protodef(https://github.com/derekwyatt/vim-protodef )。vim-protodef 依赖 FSwitch(https://github.com/derekwyatt/vim-fswitch ),请一并安装。请增加如下设置信息: ``` " 成员函数的实现顺序与声明顺序一致 let g:disable_protodef_sorting=1 ``` protodef 根据文件名进行关联,比如,MyClass.h 与 MyClass.cpp 是一对接口和实现文件,MyClass.h 中接口为: ``` " 设置 pullproto.pl 脚本路径 let g:protodefprotogetter='~/.vim/bundle/protodef/pullproto.pl' " 成员函数的实现顺序与声明顺序一致 let g:disable_protodef_sorting=1 ``` pullproto.pl 是 protodef 自带的 perl 脚本,默认位于 ~/.vim 目录,由于改用 pathogen 管理插件,所以路径需重新设置。 protodef 根据文件名进行关联,比如,MyClass.h 与 MyClass.cpp 是一对接口和实现文件,MyClass.h 中接口为: ``` class MyClass { public: void printMsg (int = 16); virtual int getSize (void) const; virtual void doNothing (void) const = 0; virtual ~MyClass (); private: int num_; }; ``` 在 MyClass.cpp 中生成成员函数的实现框架,如下图所示:(接口生成实现)
5.5 库信息参考
有过 win32 SDK 开发经验的朋友对 MSDN 或多或少有些迷恋吧,对于多达 7、8 个参数的 API,如果没有一套函数功能描述、参数讲解、返回值说明的文档,那么软件开发将是人间炼狱。别急,vim 也能做到。 要使用该功能,系统中必须先安装对应 man。安装 linux 系统函数 man,先下载(https://www.kernel.org/doc/man-pages/download.html ),解压后将 man1/ 至 man8/ 拷贝至 /usr/share/man/,运行 man fork 确认是否安装成功。安装 C++ 标准库 man,先下载(ftp://GCC.gnu.org/pub/GCC/libstdc++/doxygen/ ),选择最新 libstdc++-api-X.X.X.man.tar.bz2,解压后将 man3/ 拷贝至 /usr/share/man/,运行 man std::vector 确认是否安装成功; vim 内置的 man.vim 插件可以查看已安装的 man,需在 .vimrc 中配置启动时自动加载该插件: ``` " 启用:Man命令查看各类man信息 source $VIMRUNTIME/ftplugin/man.vim " 定义:Man命令查看各类man信息的快捷键 nmap(库信息参考)
另外,我们编码时通常都是先声明使用 std 名字空间,在使用某个标准库中的类时前不会添加 std:: 前缀,所以 vim 取到的当前光标所在单词中也不会含有 std:: 前缀,而,C++ 标准库所有 man 文件名均有 std:: 前缀,所以必须将所有文件的 std:: 前缀去掉才能让 :Man 找到正确的 man 文件。在 libstdc++-api-X.X.X.man/man3/ 执行批量重命名以取消所有 man文件的 std:: 前缀:
rename "std::" "" std::\*
顺便说下,很多人以为 rename 命令只是 mv 命令的简单封装,非也,在重命名方面,rename 太专业了,远非 mv 可触及滴,就拿上例来说,mv 必须结合 sed 才能达到这样的效果。
我认为,好的库信息参考手册不仅有对参数、返回值的描述,还应有使用范例,上面介绍的 linux 系统函数 man 做到了,C++ 标准库 man 还未达到我要求。所以,若有网络条件,我更愿意选择查看在线参考,C++ 推荐 http://www.cplusplus.com/reference/ 、http://en.cppreference.com/w/Cppreference:Archives ,前者范例多、后者更新勤;UNIX 推荐 http://pubs.opengroup.org/onlinepubs/9699919799/functions/contents.html 、http://man7.org/linux/man-pages/dir_all_alphabetic.html ,前者基于最新 SUS(Single UNIX Specification,单一 UNIX 规范)、后者偏重 linux 扩展。