目录

使用代码片段编写 LaTeX 文档

LaTeX 文档的源码有很多格式化语法,例如插图、表格等环境。使用代码片段可以很容易地插入这种结构化代码,加快文档的编写。本文介绍代码片段插件的使用方法,并介绍我自己的设计思路。

顾名思义,代码片段就是一小段可以重复利用的代码。使用代码片段本质上类似于复制粘贴并修改的操作,但利用关键字触发可以避免复制代码的过程;不仅如此,通过插件可以对设定的代码片段进行进一步操作,大大提高编程效率。

对于 Vim 或者 NeoVim 用户,可以使用 UltiSnips,我在之前的 NeoVim 基本配置 中简要介绍了其安装方法和基本用法;对于 VS Code 用户,可以使用 HyperSnips 插件。这两款插件之间的对比如下表所示:

项目UltiSnipsHyperSnips
文件路径<vimroot>/UltiSnips/HyperSnips: Open Snippets Directory
文件命名<filetype>.snippets<filetype>.hsnips
刷新插件:call UltiSnips#RefreshSnippets()HyperSnips: Reload Snippets
编程接口PythonJavaScript
触发片段自动触发或自定义快捷键自动触发或 <Tab> 键

其中,斜体表示该操作可以通过 VS Code 界面键入 <Ctrl><Shift>p 打开命令面板搜索得到。

在编写代码片段时,两个插件具有非常相似的语法,基本结构如下:

snippet trigger "description" options
snippet body
endsnippet

snippet 和 endsnippet 是编写代码片段的关键字,标志着代码片段定义的开始和结尾(抱歉这里的代码暂时不支持高亮)。

trigger 是触发代码片段的关键字,当编写文档时输入关键字并满足 options 约束的触发条件后,将会展开代码片段。一般情况下,两个插件的 trigger 可以直接设置为任意字符,如 fig。如果需要使用正则表达式,UltiSnips 需要将 trigger 使用单引号或双引号括起来,并在 options 中加入 r 选项;HyperSnips 只需要将 trigger 使用反引号 ` 括起来即可。

"description" 是对当前代码片段功能的描述,需要使用引号括起。通常情况下该参数不是必须的,但是为了使用后面的 options 选项(对位置敏感),需要补充该简要说明。

options 是触发选项,即编辑时键入关键字后还应当满足触发条件才能够展开代码片段,可选项有:

  • w:单词边界触发。这是默认的触发选项,当关键字两端不存在其他非空字符时可以触发代码片段;
  • b:行首触发。当关键子出现在单独一行的行首时可以触发代码片段;
  • i:单词内触发:只要出现关键字就可以触发代码片段;
  • r:(只在 UltiSnips 中有该选项)启用正则表达式;
  • A:自动展开。当关键字可以触发代码片段时自动展开,通常与其他选项结合使用。

snippet body 是自定义的代码片段,即触发关键字后展开的内容。代码片段内可以使用 $ 和数字设置跳点,这样可以使用设置的快捷键快速跳转:UltiSnips 需要自行设置,见 NeoVim 基本配置;VS Code 使用 <Tab> 键跳至下一个跳点,使用 <Shift><Tab> 跳到上一个跳点。特别地,$0 为最后一个跳点,跳至这里后无法再进行回跳。代码片段内的跳点可以进一步使用花括号和冒号设置默认值。

下面用一个例子进行解释(两个插件通用):

snippet beg "begin an environment" bA
\begin{${1:document}}
	$0
\end{${1:document}}
endsnippet

该片段定义了触发关键字 beg 以开始一个新的 LaTeX 环境,bA 设置该代码片段当且仅当 beg 出现在行首时自动展开。展开后,原本输入的 beg 将被代码片段的内容替换,且 \begin{} 和 \end{} 花括号里的内容将默认填充为 document 并以高亮选中可供替换。按下跳转键后光标将进入环境内部,由于该位置为 $0,该代码片段使用结束。

除了使用 options 对代码片段的触发条件进行设置,两个插件还提供了 context 功能进一步限定代码片段的触发条件:在 snippet 前一行通过 context 调用 Python (或 JavaScript)函数,只有当该函数返回 True 时,后面的代码片段可以触发。利用这个功能,我们可以检测当前光标位置所处的环境,当处于数学环境中时才启用某些代码片段,以减少误触发的概率。

对于 Vim/NeoVim 用户,UltiSnips 在处理 LaTeX 文档时需要借用 vimtex 插件提供的 vimtex#syntax#in_mathzone() 函数;而将其应用于 Markdown 时,还要进一步通过 vim-markdown 插件使该能够判断 Markdown 文件的数学环境。如此做,在代码片段文件的开头做如下函数定义:

global !p
def math():
	return vim.eval('vimtex#syntax#in_mathzone()') == '1'
endglobal

然后在 snippet 的前一行加入 context "math()" 即可。

对于 VS Code 用户,可以使用 <Ctrl><Shift>p 打开命令面板,搜索使用 Developer: Inspect Editor Tokens and Scopes,通过 scopes 属性查看当前光标所在的环境。相应地,HyperSnips 可以定义类似的 JavaScript 函数用于数学环境的判断:

global
function math(context){
    return context.scopes.some(s => s.includes(".math"));
}
endglobal

不同于 UltiSnips 的用法, HyperSnips 在 snippet 的前一行加入的命令为 context math(context)。

如前所述,UltiSnips 和 HyperSnips 分别支持在代码片段内使用 Python 和 JavaScript 作为编程接口。这通常与正则表达式联合使用,可以更加灵活地处理代码片段。UltiSnips 中使用编程接口时,Python 代码需要使用 `!p 和 ` 包裹;而 HyperSnips 的 JavaScript 代码需要用 `` 在两端包裹。两个插件提供了默认变量用于接口,常用的变量如下表所示( UltiSnips 提供更多变量,使用 :h UltiSnips-python 查看)

关键字内容UltiSnipsHyperSnips
当前文件路径pathpath
代码片段各跳点的内容tt
正则表达式各分组匹配的内容match.groupm
编程返回的代码片段snip.rvrv

由于本人对 JavaScript 所知甚少,下面用 Python 接口为例进行解释。

context "math()"
snippet '([A-Za-z]+)rm' "mathrm" rA
`!p snip.rv = '\\mathrm{' + match.group(1) + '}' `$0
endsnippet

该 UltiSnips 代码片段的第一行约束了该片段只在数学模式下生效。触发关键词使用引号,结合触发选项 r 说明使用了正则表达式:圆括号对字符串做了一个分组,匹配之后该分组的内容可以使用 match.group(1) 获取;分组内的中括号指明该分组匹配的内容为大小写字母,后面的加号表示至少有一个字母;圆括号外的字符表示该关键字必须以 rm 结尾(这就是所谓的“后缀式触发”)。该代码片段实现的效果是:在数学模式下,在字符后面加入 rm 可以将前面的字符变成 \mathrm{} 命令包围的直立字体。更多关于正则表达式的用法可以参考 菜鸟教程 等技术教程。

为了偷懒,代码片段的关键字应当尽可能少,使用 2~3 个字符较为合理。并且根据 Elijan 的建议,尽可能使用自动展开加快编写速度。

为此,我定义了下列关键字用于触发 LaTeX 中的各种环境或命令:

关键字功能触发条件
env新建 LaTeX 环境行首触发
beg新建 LaTeX 环境行首触发
fig新建插图环境行首触发
tabu新建普通表格行首触发
tabx新建定宽表格行首触发
tabl插入三线表的横线行首触发
equ单行公式环境行首触发
gat居中对齐的多行公式环境行首触发
ali手动对齐的多行公式环境行首触发
gad居中对齐公式的子环境边界触发
ald手动对齐公式的子环境边界触发
pmat圆括号包围的矩阵环境边界触发
bmat方括号包围的矩阵环境边界触发
Bmat花括号包围的矩阵环境边界触发
vmat单竖线包围的矩阵环境边界触发
Vmat双竖线包围的矩阵环境边界触发
ff插入数学分式边界触发
lr(自动大小的圆括号边界触发
lr[自动大小的方括号边界触发
lr{自动大小的花括号边界触发
lr<自动大小的尖括号边界触发
ab|自动大小的绝对值符号边界触发
no|自动大小的范数符号边界触发
b(手动大圆括号边界触发
b2(手动更大的圆括号边界触发
b3(手动超级大的圆括号边界触发
b4(手动究极大的圆括号边界触发

使用正则表达式结合词内触发可以实现更常用的后缀式触发,这在对数学符号进行修饰时非常好用。例如上面演示的触发正体命令。类似地,我做了如下定义(用 X 表示后缀前面的内容,不能出现空格):

后缀命令说明
rm\mathrm{X}将变量变为直立体
bf\mathbf{X}将变量变为粗体
fk\mathfrak{X}将变量变为哥特体
bm\bm{X}使用 bm 宏包加粗变量
iiX^{-1}求逆
sr\sqrt{X}开方
vec\vec{X}箭头(矢量符号)
dot\dot{X}一个点(一阶导数)
ddo\ddot{X}两个点(二阶导数)
hat\hat{X}尖帽子(估计值)
bar\bar{X}横线(平均值)
bre\breve{X}圆帽子(测量值)
til\tilde{X}波浪线(误差)

使用后缀触发可以实现更加便利的功能:

  • 分式自动处理

1500 页数学笔记的小哥在他的 博客 中给出了自动处理分式的代码片段:在数学模式下,当输入除法 / 时,会自动将前面的字符作为 \frac 的分子,并跳转到分母位置。当分子不止一个变量时,可以使用圆括号括起后再输入 /,此时分子会自动取消最外围用于定界的圆括号。

  • 角标自动处理

数学通常会与角标打交道,为此,我在数学模式下定义了代码片段:任意字符;下标;上标; 。该代码片段在感受第三个封号后会自动处理角标,并且会根据字符长度选择性地添加 {}。下标或上标可以置空,但不允许出现空格。角标中处理特殊符号时可以使用空的花括号分隔命令,例如 \omega{}t 。

上面讨论的 UltiSnips 和 HyperSnips 代码片段可以分别在我 nvim 仓库 和 LaTeX 仓库 找到,欢迎使用和分享建议。

  1. Gilles Castel. How I’m able to take notes in mathematics lectures using LaTeX and Vim.
  2. Elijan J. Mastnak. An UltiSnips guide for LaTeX workflows.