snippet
安装
一个 snippet 是一段可重复使用的短文本,可用来编辑其他文本。例如,
当我键入sign
并按下Tab
时,单词sign
将会补全为一个自定义的签名文本。
Snippet 也可以是动态的:当我键入today
并按下Tab
时,
单词today
将会被当前日期替代;键入box
Tab
变成一个可以自动增大的框。
我使用插件 UltiSnip 来管理我的 snippet。 vim-snippets是网上收集的各种常用的snippets。
Plugin 'sirver/ultisnips' Plugin 'honza/vim-snippets'
读取Ultisnips的路径是~/.vim/UltiSnips
。但是也可以自定义。
配置路径:
let g:UltiSnipsSnippetDirectories=['UltiSnips'] # 模板路径 let g:UltiSnipsExpandTrigger="<tab>" # 补全键 let g:UltiSnipsJumpForwardTrigger="<c-j>" # 下一下变量位置 let g:UltiSnipsJumpBackwardTrigger="<c-k>" # 上一个变量位置
定义自己的snippet
读取Ultisnips的路径是~/.vim/UltiSnips
。但是也可以自定义。
如果安装了honza/vim-snippets
的话,UltiSnips加载的路径会被vim-snippets
覆盖,所以要把配置文件放到vim-snippets
的vim-snippets/UltiSnips
目录下。
通过命令UltiSnipsEdit
打开一个与当前文件扩展名相关的配置文件。
注意,这里有一个BUG:Vim回到的snips文件是绝对路径,但UltiSnipsEdit
编辑的是
相对路径下的文件(以vim打开时所在的位置为标准)。所以编辑的文件和vim调用的
不是同一个文件。需要手动把文件复制到绝对目录下。
定义Snippets
基本格式
snippet trigger "Description" option ... endsnippet
-
trigger
:关键字 -
Description
:备注 -
option
:选项,包含:-
w
:默认开启, 只有是一个单词(前面是空格) 才可以使用. -
i
:与w
相反情况下, 即使是在单词中, 只要出现了这几个字母, 比如aaatrigger
, 也可以使用 -
b
:只有当trigger在行首才有效 -
r
:支持正则表达式t在这里,的其他功能失效, 就当成空格使用. -
A
:trigger不需要按tab就可以自动展开.
-
位置跳转
snippet trigger "Description" option if (${1:${VISUAL:contents}}) { $2 } else { $3 } $0 endsnippet
-
\(1
、\)2
、等是光标的跳转位置,按tab
时跳到下一个。如果有两个编号一样。 那么输入一个时,另一个位置会自动变成相同的内容。 -
$0
所有的位置都跳过以后最后的位置。 -
${VISUAL:contents}
:当光标位于这里时,contents
这一段的内容是显示出来的。
正则与begin-end
snippet "b(egin)?" "begin{} / end{}" br \begin{${1:something}} ${0:${VISUAL}} \end{$1} endsnippet
-
b
:只有当trigger在行首才有效 -
r
:支持正则表达式在这里的其他功能失效 就当成空格使用 -
b(egin)
:正则表达式内容。b
或begin
, 都能用这个代码块展开 -
在代码区有两个
$1
,那么就是说只要我们在\begin
部分输入了somethingelse
, 那么在\end
区,也自动有somethingelse
还可以进一步改成这样:
snippet "b(egin)?" "begin{} / end{}" br \begin{${1:something}} ${2:${VISUAL}Write your words here} \end{$1}$0
这样继续tab
就可以跳出区域了。这时也可以配合latex-suite插件使用,
在$0
处换成<++>
这样用<Ctrl-J>
就可以跳出环境了
调用python代码
snippet '([A-Za-z])([\d])' "auto subscript" wrA `!p snip.rv = match.group(1)`_`!p snip.rv = match.group(2)` endsnippet
选项部分是wrA
。A
代表自动展开。同样地,我们用到了正则表达式。
在代码区,我们还看到了有snip.rv
。这时用了python的情况。!p snip.rv =
这后面就是要输出的部分 。
在trigger里,我们看到两个括号,一个是([a-z])
,另一个是([\d])
,这是正则表达式,
代表所有大小写字母数字,匹配到match.group(1)
和match.group(2)
。
从code里看出,这两个代码块用_
下划线连接了 。
那么好, 如果我们用了这个代码块,输入x1
, 就会自动输出x_1
。
如果在数学公式环境里,这么自动展开还可以,在非数学模式,这么做显然有很多不便。 那么可以规定数学环境吗?答案是可以 。
指定环境
我们定义math 环境如下, 并把这段代码放在snippets文档的开头部分.
global !p texMathZones = ['texMathZone'+x for x in ['A', 'AS', 'B', 'BS', 'C', 'CS', 'D', 'DS', 'E', 'ES', 'F', 'FS', 'G', 'GS', 'H', 'HS', 'I', 'IS', 'J', 'JS', 'K', 'KS', 'L', 'LS', 'DS', 'V', 'W', 'X', 'Y', 'Z']] texIgnoreMathZones = ['texMathText'] texMathZoneIds = vim.eval('map('+str(texMathZones)+", 'hlID(v:val)')") texIgnoreMathZoneIds = vim.eval('map('+str(texIgnoreMathZones)+", 'hlID(v:val)')") ignore = texIgnoreMathZoneIds[0] def math(): synstackids = vim.eval("synstack(line('.'), col('.') - (col('.')>=2 ? 1 : 0))") try: first = next( i for i in reversed(synstackids) if i in texIgnoreMathZoneIds or i in texMathZoneIds ) return first != ignore except StopIteration: return False endglobal
定义好了以后,在上面的代码前加上一行,变成:
context "math()" snippet '([A-Za-z])([\d])' "auto subscript" wrA `!p snip.rv = match.group(1)`_`!p snip.rv = match.group(2)` endsnippet
这样就只有在数学公式的模式下才有用了。
添自定义的snippets命令
-
可以直接在UltiSnips插件目录下的
tex.snippets
文档里添加。但这样修改了原来插件中的代码,有风险的。 -
可以建一个文档:
~/.vim/UltiSnips/tex.snippets
你的新定义的代码块放在这里 -
推荐:通过
:UltiSnipsEdit
命令
基本工具
定义静态文本
定义 sign 的 snippet 的代码如下所示:
snippet sign "Signature" Yours sincerely, Gilles Castel endsnippet
定义动态文本
对于动态 snippet,你可以在倒引号之间输入代码,并在 snippet 扩展时运行。
我在这里使用 bash 编译器格式化当前日期:date + %F
snippet today "Date" `date +%F` endsnippet
用Python生成文本
你也可以在!p ...
块内部使用 Python。例如要生成了一个动态随着内容变大的文本框。
你可以看一下定义 box 的 snippet 代码:
snippet box "Box" `!p snip.rv = '┌' + '─' * (len(t[1]) + 2) + '┐'` │ $1 │ `!p snip.rv = '└' + '─' * (len(t[1]) + 2) + '┘'` $0 endsnippet
这些 Python 代码块将会被变量snip.rv
的值替代。在这些代码块内部,
你可以访问 snippet 的当前状态,如t[1]
包含第一个制表位,fn
表示当前文档名称。
后缀 snippet
值得分享的还有后缀 snippet,如phat → \hat{p}
,zbar → \overline{z}
等。
一个类似的 snippet 是后缀向量,如v → \vec{v}
。这些 snippet 真的非常节省时间,
因为你可以借此跟上老师板书的节奏。注意,我还是可以用 bar 和 hat 前缀,
我以较低的优先级将它们添了进去。那些 snippet 的代码如下:
priority 10 snippet "bar" "bar" riA \overline{$1}$0 endsnippet priority 100 snippet "([a-zA-Z])bar" "bar" riA \overline{`!p snip.rv=match.group(1)`} endsnippet priority 10 snippet "hat" "hat" riA \hat{$1}$0 endsnippet priority 100 snippet "([a-zA-Z])hat" "hat" riA \hat{`!p snip.rv=match.group(1)`} endsnippet snippet "(\\?\w+)(,\.|\.,)" "Vector postfix" riA \vec{`!p snip.rv=match.group(1)`} endsnippet
常用snippet例子
LaTex
环境
snippet beg "begin{} / end{}" bA \begin{$1} $0 \end{$1} endsnippet
-
b
意味着该 snippet 将只能在一行开端扩展, -
A
代表自动扩展,这意味着我不需要键入 Tab 就可以扩展 snippet。 -
制表位--即可以通过按下
Tab
和Shift+Tab
跳转到的地方--以$1
、$2
等表示, 同时最后一个为$0
。
行内公式与块公式
我最常使用的两个 snippet 是 mk 和 dm。这些 snippet 负责数学代码的开始。 第一个是 inline math snippet,第二个是 display math snippet。
该 snippet 的代码如下所示:
snippet mk "Math" wA $${1}$`!pif t[2] and t[2][0] not in [',', '.', '?', '-', ' ']: snip.rv = ' 'else: snip.rv = '' `$2endsnippet
第一行结尾处的w
意味着该 snippet 将在词边界扩展,所以举例而言,hellomk 不会扩展,而 hello mk 会扩展。
inline math snippet 是「智能的」:它知道何时在$
符号后嵌入一个位置。
当我在结尾$
的正后方开始键入一个单词时,它添加一个位置。
但是,当我键入一个非单词字符时,它不添加一个位置,
例如下图的p
变量值:
diaplay math snippet 更简单,但同时也相当方便;该 snippet 使我在一段时间内不会忘记结束方程式。
snippet dm "Math" wA \[ $1 .\] $0 endsnippet
上下标
该 snippet 代码使用正则表达式作为触发器。
当你在\([A-Za-z]\d\)编码的数字后面键入一个字符,
或者在_
以及两个数字\([A-Za-z]_\d\d\)后面键入一个字符时,触发器会扩展该 snippet
snippet '([A-Za-z])(\d)' "auto subscript" wrA `!p snip.rv = match.group(1)`_`!p snip.rv = match.group(2)` endsnippet snippet '([A-Za-z])_(\d\d)' "auto subscript2" wrA `!p snip.rv = match.group(1)`_{`!p snip.rv = match.group(2)`} endsnippet
当你在使用圆括弧包装部分正则表达式时,如(\d\d)
,你可以通过 Python 中的
match.group(i)
在扩展 snippet 中使用它们。
对于上标而言,我使用td
自动扩展为^{}
的。但是,对于平方、立方、
以及一小部分其他常见上标,我使用sr
、cb
、comp
等专门的 snippet。
snippet sr "^2" iA ^2 endsnippet snippet cb "^3" iA ^3 endsnippet snippet compl "complement" iA ^{c} endsnippet snippet td "superscript" iA ^{$1}$0 endsnippet
分数
用于分数的 snippet 是我使用最方便的自动扩展方法之一,它可以进行以下扩展:
第一个的代码非常简单:
snippet // "Fraction" iA \\frac{$1}{$2}$0 endsnippet
第二和第三个例子使用正则表达式匹配3/
、4ac
、6\pi^2/
、a_2/
等表达式。
snippet '((\d+)|(\d*)(\\)?([A-Za-z]+)((\^|_)(\{\d+\}|\d))*)/' "Fraction" wrA \\frac{`!p snip.rv = match.group(1)`}{$1}$0 endsnippet
在第四、第五个例子中,它试图找到匹配圆括弧。由于不能使用 UltiSnips 的正则表达式引擎,我转向了 Python:
priority 1000 snippet '^.*\)/' "() Fraction" wrA `!p stripped = match.string[:-1] depth = 0 i = len(stripped) - 1 while True: if stripped[i] == ')': depth += 1 if stripped[i] == '(': depth -= 1 if depth == 0: break; i -= 1 snip.rv = stripped[0:i] + "\\frac{" + stripped[i+1:-1] + "}" `{$1}$0 endsnippet
最后一个与分数有关的 snippet 即使用你的选择来制作分数。
你可以先用它选择一些文本,然后按下Tab
,打出/
,然后再按下Tab
。
这些代码用到了${VISUAL}
变量表示你的选择。
snippet / "Fraction" iA \\frac{${VISUAL}}{$1}$0 endsnippet
其他
我还有 100 多个其他常用的 snippet,其中多数非常简单。例如,!>
变成\mapsto
,
->
变成\to
等。放在../code/vim/tex-custom.snippets
:
sympy 和 Mathematica
另一个很酷但不太常用的 snippet 是利用 sympy 评估数学表达式。例如,
sympy Tab 键扩展为sympy | sympy,sympy 1 + 1 sympy Tab
键扩展为 2。
snippet sympy "sympy block " w sympy $1 sympy$0 endsnippet priority 10000 snippet 'sympy(.*)sympy' "evaluate sympy" wr `!p from sympy import * x, y, z, t = symbols('x y z t') k, m, n = symbols('k m n', integer=True) f, g, h = symbols('f g h', cls=Function) init_printing() snip.rv = eval('latex(' + match.group(1).replace('\\', '') \ .replace('^', '**') \ .replace('{', '(') \ .replace('}', ')') + ')') ` endsnippet
Mathematica 用户也可以进行类似的操作:
priority 1000 snippet math "mathematica block" w math $1 math$0 endsnippet priority 10000 snippet 'math(.*)math' "evaluate mathematica" wr `!p import subprocess code = 'ToString[' + match.group(1) + ', TeXForm]' snip.rv = subprocess.check_output(['wolframscript', '-code', code]) ` endsnippet