Jade Dungeon

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-snippetsvim-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):正则表达式内容。bbegin, 都能用这个代码块展开
  • 在代码区有两个$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

选项部分是wrAA代表自动展开。同样地,我们用到了正则表达式。

在代码区,我们还看到了有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 的代码如下:

vim.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

环境

vim.snippet

snippet beg "begin{} / end{}" bA
\begin{$1}
    $0
\end{$1}
endsnippet
  • b意味着该 snippet 将只能在一行开端扩展,
  • A代表自动扩展,这意味着我不需要键入 Tab 就可以扩展 snippet。
  • 制表位--即可以通过按下TabShift+Tab跳转到的地方--以$1$2等表示, 同时最后一个为$0

行内公式与块公式

我最常使用的两个 snippet 是 mk 和 dm。这些 snippet 负责数学代码的开始。 第一个是 inline math snippet,第二个是 display math snippet。

vim.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 会扩展。

in­line math snippet 是「智能的」:它知道何时在$符号后嵌入一个位置。 当我在结尾$的正后方开始键入一个单词时,它添加一个位置。 但是,当我键入一个非单词字符时,它不添加一个位置, 例如下图的p变量值:

vim.snippet

diaplay math snippet 更简单,但同时也相当方便;该 snippet 使我在一段时间内不会忘记结束方程式。

snippet dm "Math" wA
\[
$1
.\] $0
endsnippet

上下标

vim.snippet

该 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自动扩展为^{}的。但是,对于平方、立方、 以及一小部分其他常见上标,我使用srcbcomp等专门的 snippet。

vim.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 是我使用最方便的自动扩展方法之一,它可以进行以下扩展:

vim.snippet

vim.snippet

第一个的代码非常简单:

snippet // "Fraction" iA
\\frac{$1}{$2}$0
endsnippet

第二和第三个例子使用正则表达式匹配3/4ac6\pi^2/a_2/等表达式。

snippet '((\d+)|(\d*)(\\)?([A-Za-z]+)((\^|_)(\{\d+\}|\d))*)/' "Fraction" wrA
\\frac{`!p snip.rv = match.group(1)`}{$1}$0
endsnippet

vim.snippet

在第四、第五个例子中,它试图找到匹配圆括弧。由于不能使用 Ul­tiSnips 的正则表达式引擎,我转向了 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

vim.snippet

这些代码用到了${VISUAL}变量表示你的选择。

snippet / "Fraction" iA
\\frac{${VISUAL}}{$1}$0
endsnippet

其他

我还有 100 多个其他常用的 snippet,其中多数非常简单。例如,!>变成\mapsto->变成\to等。放在../code/vim/tex-custom.snippets

vim.snippet

sympy 和 Mathematica

另一个很酷但不太常用的 snippet 是利用 sympy 评估数学表达式。例如, sympy Tab 键扩展为sympy | sympy,sympy 1 + 1 sympy Tab键扩展为 2。

vim.snippet

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 用户也可以进行类似的操作:

vim.snippet

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