vim 入坑指南(六)插件 UltiSnips

上一篇,我们说到了第一个插件 Vim-Plug,简而言之,是个管理插件的插件。这一篇,我们说一个提升我们输入效率的一个插件:UltiSnips。

很多时候,我们要输入的很多代码都是大体重复的(因为真正重要的部分是怎样把它们放在一起的过程)。那些重复的部分,就是各种 snips。把它们整理出来,想到什么就加什么。一两个星期之后,尝试关掉这个插件,就会明白使用 snips 究竟给你剩下了多少时间。这就是一个 “磨刀不误砍柴工” 的过程,投入的时间越多,回报自然也就越大。就比如我想简单的写这样一段 c++ 代码:

vim 有许多 snips 插件,我只是凑巧一开始选择了 UltiSnips。其他的插件(我猜测)套路也都是大体相当的,我不能说 UltiSnips 就是最好的,因为其他的我没用过。 UltiSnips 更像是一个调用引擎,它本身并不提供任何 snips,它在官方文档 (github.com/SirVer/ultisnips)里提到了一个专门提供 snips 的插件 vim-snippets

如何安装 UltiSnips 和 vim-snippets

有了 Vim-Plug,安装任何插件都很简单,只需要把 UltiSnipsvim-snippets 的 github 的项目名放进去,重启 vim,然后在命令行里运行 PlugInstall 就好啦。 所以,vimrc 里大概是这样的:

1
2
3
4
5
6
7
" vim-plug
call plug#begin('~/.vim/plugged')

Plug 'SirVer/ultisnips'
Plug 'honza/vim-snippets'

call plug#end()"

这些是 vim-snippets 提供的 snips:

UltiSnips 的基本要求和工作原理

要使用 UltiSnips,vim 需要开启对 python 的支持。可以供过一下这两个方式来验证你的 vim 是否支持 python 接口:

1
2
:echo has("python")   " 如果你用的是 python 2.7
:echo has("python3") " 如果你用的是 python 3.3 或者 3.4

或者你也可以通过 version 调出所有目前 vim 所有支持与不支持的接口。

1
:version

如果是 +python 或者 +python3 就没问题。V 字君用的是 2.7,所以我的是 +python-python3,有一个就成。如果两个都没有,可能是你没有安装 python,或者说安装 vim 的时候系统找不到 python 的路径(一般 windows 上会出现这个问题,小伙伴要自己谷歌百度一下),折腾一下也就没问题啦。

当我们打开 vim 的时候, UltiSnips 会搜寻 $VIM 路径下的所有名字为 UltiSnips 的文件夹,然后根据文档类型来寻找对应的 snips。就比如开头的那段代码,有一些是由 vim-snippets 提供的,一些是自己定义的,比如

  • incc 会展开成 include <iostream>
  • main 会展开成一段 main function
  • cout 会展开成一段 cout << ... << endl;

UltiSnips 会展开一段文本,替换成你提前定义好的 snips。你可以定义是否自动展开,snip 里的内容跳转,不着急。我们一步一步说。

如何展开一段 snip

UltiSnips 的自带设定是 <tab>。我们输入 main 后按下 <tab> 键,snip 就会展开了。

你可以在 vimrc 里通过 g:UltiSnipsExpandTrigger 来更改“展开键”

1
let g:UltiSnipsExpandTrigger = 你想用的键

如何自定义简单的 snips

最直接的办法就是我们在 $VIM 路径下新建一个 UltiSnips 的文件夹,然后把所有自定义的 snips 都放进去。如果你对 git 很熟悉的话,你也可以的把这个文件夹放进 git,甚至是一个单独的 git 代码库里 (很多人都有自己的 dotfiles 代码库,然后放到 github 上)。

不说太复杂的,在这里我们就用最直接的方法。创建了 UltiSnips 之后,然后在里面创建一个文件:cpp.snippets,所有我们自定义的 c++ snips 都会放在这里。一般来说, snippets 的文件名称就是 文档类型 + .snippets,如果是 LaTex 的文件,那就是 tex.snippets,以此类推。就像之前 vim-snippets 的例子一样。

我们从简单的说起,我们先放进入这段代码到 cpp.snippets 里,保存之后进入一个 c++ 文件,看看有没有用:

1
2
3
snippet std "use namespace std" b
using namespace std;
endsnippet

UltiSnips 会自动抓取最新的修改,所以我们不用重启 vim,只要保存了 snippet 文件,就可以直接使用。简单来说,定义的一段 snip 的格式是这样的:

1
2
3
snippet 关键词 “说明” 设定
内容
endsnippet

在刚才那段 snip 里,std 就是我们可以展开的 关键词"use namespace std" 是一段说明(如果你用一些自动补全的插件,这段说明就会显示出来,我们之后自然会提到,放心),而 b 的是关于这个 snip 的设定,

  • b 代表只有关键词出现在行首的时候,才可以被展开
  • A 代表自动展开
  • w 代表可以展开这个 “词”,具体 “词” 的定义可以查看 :help iskeyword。直观感觉就是,这个关键词是单独的,和其他文字分开的。比如前后都是空格。
  • i 代表可以忽略前后字节,直接展开关键词。(这个设定比 w 要更松)

其他的还有 r, s, t, m 等等,都可以通过这个命令在自带文档里找到:

1
:help ultisnip

UltiSnip 进阶

现在,咱们做点复杂的。比如在 c++ 里,我们经常要定义 class,我们先把这段 snip 放进 cpp.snippets,然后我们慢慢解释。

1
2
3
4
5
6
7
8
9
10
11
12
snippet class "class" b
class ${1:Class}{
public:
// constructors, asssignment, destructor
$1();
$1(const $1&);
$1& operator=(const $1&);
~$1();
private:

};
endsnippet

然后我们进入一个 c++ 文件,输入 class 然后按下 <tab> 展开。展开之后,敲入 BinaryTree

很神奇对吧?现在我们回来说 snip。 里面的 ${1:Class} 就是代表可以替换的内容,如果不替换的话则会显示冒号后面的内容,在这个例子是就是 Class。而且其他几个 $1 则会显示一模一样的内容。

这里的 $1 也代表跳转的内容。比如我们可以再加上 $2,$3,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
snippet class "class" b
class ${1:Class}{
public:
// constructors, asssignment, destructor
$1();
$1(const $1&);
$1& operator=(const $1&);
~$1();
$2
private:
$3
};
endsnippet

UltiSnips 里,自带的跳转设定是

1
2
<c-j> " 跳至下一个部分
<c-k> " 跳至前一个部分

开始相同的操作,输入 class 然后按下 展开。展开之后,敲入 BinaryTree。然后按下 <c-j> (ctrl + j) 就会跳之下一个部分。

多试几次就熟练啦。

在 UltiSnips 里使用 Visual

在这里会出现一个问题:虽然 snip 好用,但是想把现有代码放进定义好的 snip 里面的效率并不高。假设我们有这么一段用来定义函数的 snip:

1
2
3
4
5
snippet fun "function def" b
${2:void} ${1:name}($3){
$4
}
endsnippet

我想把这样一行写好的代码放进单独的函数里:

1
cout << "Vim Rocks!" << endl;

那么要做的操作就是:

  1. 输入 fun,按下 <tab> 展开
  2. 然后把现有那行代码放进去

如果我们在 snip 里使用 visual 的话,就可以省去第二步。我们先把 snip 改动一下

1
2
3
4
5
snippet fun "function def" b
${2:void} ${1:name}($3){
${4:${VISUAL}} // <--- 这行加了个 ${VISUAL}
}
endsnippet

这里的 visual 指的就是 vim 里的 visual。而 ${VISUAL} 的意思就是会把 visual 选中的文本放进这里。我们来操作一下:

  1. visual 模式下选中已有代码,按下 <tab>
  2. 然后按老样子输入并展开 fun

这样一来,很多类似的问题就都解决了,而且省时省力。在写 LaTex 代码的时候我发现这个功能帮我提速了不少,目前基本上我所有上课的笔记都是用 LaTex 打的,用的插件则是 vimtex,之后咱们单开一篇提(再挖一坑)。

在 UltiSnips 里使用 python

对于更多复杂的处理,我们可以借助 python 来替我们完成。在这里我们就举个最简单的例子,插入文件夹里的所有文件名。在 cpp.snippets 开头,我们加入这么一段代码,!p 代表 python,如果你想用 vimscript 的话就是 !v

1
2
3
4
5
6
7
8
global !p
def list_files():
files = []
for f in os.listdir('.'):
if f.endswith(('.cpp', '.h', '.cc')) and not f.startswith('.'):
files.append(f)
return ' '.join(files)
endglobal

在之后,我们加入一段使用的这段 python 代码的 snip。要说明的是,snip.rv 代表的是 python 返回的内容 (snip return value)

1
2
3
snippet ls "list source files" iw
`!p snip.rv = list_files()`
endsnippet

假设我现在同一个文件夹里有两个文件:ultisnip.cppultisnips.h,现在我们来试一试:

好啦,目前我能想到的就是这么多。UltiSnips 是我认为和输入效率最相关的插件(之一),它涉及到你对自己写码的习惯的了解,以及对相应文本的了解。其实,snip 的感觉就像是对 关键词 定义了个函数 (function)。更多的使用 snip 不仅能提升输入效率,也可以减少 犯错几率,因为我只要确保那一份 snip 是对的就成了。而与之搭配的自动补全的插件也有很多,其中比较有名的就是 YouCompleteMe,这个插件我们以后也会提到。

感谢大家对 Vimpress 的支持,我虽然不能保证周周更新,但是每一篇都会认真对待的。下一篇写什么我还没有想好,上星期刚考完期末,现在又要开学了,我继续去写作业了…

VZJ wechat
欢迎您扫一扫上面的二维码,订阅 V 字君的微信公众号。