本文是作者回答 我们能否用 LaTeX 记笔记跟得上讲座的速度,生发出来的博客文章。作者介绍了其工具和基本的操作思路。首先看看作者自己记录的笔记效果,洋洋洒洒 1700 页的内容:

这些讲义 - 包括数字 - 是在参加讲座时制作的,之后没有经过任何的编辑。为了说明使用 LaTeX 记笔记的可行性,作者专门制定了目标:第一个是在LaTeX中编写文本和数学公式应该与讲师在黑板上书写一样快:没有延迟。绘图数字几乎与讲师一样快。

作者所用的神器是  LaTeX + Vim。

上手 LaTeX

我们看看用Vim编辑LaTeX的场景,如下:

main-qimg-3bfe4188af07b30e8131cbbdbbeceb05.png

左边是Vim,右边是 pdf 阅读器 Zathura,它也有类似 Vim 的快捷键。

在 Vim 中,使用的 LaTeX 插件是 vimtex,它有语法高亮显示、目录视图、代码的同步等功能。

然后,使用 vim-plug 做如下配置:

Plug 'lervag/vimtex'
let g:tex_flavor='latex'
let g:vimtex_view_method='zathura'
let g:vimtex_quickfix_mode=0
set conceallevel=1
let g:tex_conceal='abdmg'

最后两行控制的是“隐藏”功能。开启了这个功能,除了你光标所在的那一行之外,文本里夹杂的LaTeX代码就都会隐藏或者替换成其他符号。

比如说在下面动图里,隐藏了[,],$之后,没有了它们的干扰,整个文档就更易读。这个功能还会用∩替代\bigcap,∈替代\in等等。

conceal-d362e0b837be308b0f8897482a331ec6.gif

有了这个设置,我就来到了这篇博文的关键:写作 LaTeX 的速度和老师在黑板上写的一样快。就是下面片段发挥的作用。

片段

什么是片段?

代码段是一段可重复使用的短文本,可以由其他一些文本触发。例如,当我键入 sign 并按下时 Tab,该单词 sign 将扩展为签名的片段内容:

sign-5ab28b1ad6114b68f4ce459d4f89eeae.gif

片段也可以是动态的:当我键入today并按下时Tab,该单词today将被当前日期替换,并box Tab成为一个自动增大的框。

你甚至可以在另一个内部使用一个片段:

todaybox-cdd1fded48947d163c2e5e6b974cfec0 (1).gif

怎么创建片段?使用UltiSnips

管理片段的插件 UltiSnips,是这样配置的:

Plug 'sirver/ultisnips'
let g:UltiSnipsExpandTrigger = '<tab>'
let g:UltiSnipsJumpForwardTrigger = '<tab>'
let g:UltiSnipsJumpBackwardTrigger = '<s-tab>'

代码sign段的代码如下:

snippet sign "Signature"
Yours sincerely,

Gilles Castel
endsnippet

对于动态代码段,您可以将代码放在反引号之间,这些反引号``将在代码片段展开时运行。在这里,我使用bash格式化当前日期:date + %F。

snippet today "Date"
`date +%F`
endsnippet

您也可以在`!p ... `块内使用Python 。看一下代码box段的代码:

snippet box "Box"
`!p snip.rv = '┌' + '─' * (len(t[1]) + 2) + '┐'`
│ $1 │
`!p snip.rv = '└' + '─' * (len(t[1]) + 2) + '┘'`
$0
endsnippet

这些Python代码块将被变量的值替换snip.rv。在这些块中,您可以访问代码段的当前状态,例如t[1]包含第一个制表位,fn当前文件名,...

LaTeX片段

使用片段,编写LaTeX比手动编写快得多。特别是一些更复杂的片段可以为您节省大量时间和挫折。让我们从一些简单的片段开始。

环境

要插入环境,我所要做的就是beg在一行的开头键入。然后我输入环境的名称,该名称在\end{}命令中被镜像。按下Tab将光标置于新创建的环境中。

beginend-d5ed641a9cb568d13dd56d98fec6376b.gif

此代码段的代码如下。

snippet beg "begin{} / end{}" bA
\begin{$1}
	$0
\end{$1}
endsnippet

这 b 意味着此代码段只会在行的开头A展开并代表自动展开,这意味着我不必按此 Tab 按钮展开代码段。制表符停止 - 也就是说你可以通过按 Tab 和 Shift+ 跳转到的地方 Tab 用 $1,, 表示 $2...,最后一个表示 $0。

内联和显示数学

我最常用的两个片段是 mk 和 dm。它们是负责启动数学模式的片段。第一个是内联数学的片段,第二个是显示数学的片段。

mkdm-12a16b6a90c4d85a887c7cde0f18bb8c.gif

内联数学的片段是“聪明的”:它知道何时在美元符号后插入空格。当我开始在结束后直接输入一个单词时$,它会添加一个空格。但是,当我键入非单词字符时,它不会添加空格,例如在 ‘$p$-value’

mk_space-1172a1f556fd36b42f7c3642b3952e2c.gif

此代码段的代码如下:

snippet mk "Math" wA
$${1}$`!p
if t[2] and t[2][0] not in [',', '.', '?', '-', ' ']:
    snip.rv = ' '
else:
    snip.rv = ''
`$2
endsnippet

将w在第一行的末尾意味着这个片段将扩大在单词边界,所以例如hellomk不会扩大,但hello mk会。

显示数学的片段更简单,但也非常方便; 它让我永远不会忘记用句号结束方程式。

dm-a682e97365ecfe19fe0c5bc08bacfec6.gif

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

子标题和上标

另一个有用的片段是下标。它的变化而变化 a1,以 a_1 及 a_12 对 a_{12}。

subscripts-f26633cb822c6b52d346dfbba6e1a1bd.gif

此代码段的代码使用正则表达式作为其触发器。当您键入一个字符后跟一个由其编码的数字 [A-Za-z]\d 或后跟_两个数字的字符时,它会扩展该片段:[A-Za-z]_\d\d。

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),您可以通过match.group(i)Python 在扩展代码段中使用它们。

至于上标,我使用td,成为^{}。然而,对于平方,立方,补充和少数其他常见的,我使用专用的片段,如sr,cb和comp。

superscripts-da855cd9e67c48f508fab5ddc8c877fe.gif

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

分数

我最方便的片段之一是分数片段。这使得以下扩展:

/ / → frac {}{}
3 / → frac {3}{}
4 pi ^ 2 / → frac {4 pi ^ 2}{}
(1 + 2 + 3) / → frac {1 + 2 + 3}{}
(1 + (2 + 3) /)→(1 + frac {2 + 3}{})
(1 + (2 + 3)) / → frac {1 + (2 + 3)}{

frac-3f83f1bdc3078aa16382e80a276f199f.gif

这一段代码比较简短:

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

正如您所看到的,正则表达式可能变得非常庞大,但这是一个应该解释它的图表:

regex-908df5ec254f9cd5b08b99b8a54318d0.png

在第四和第五种情况下,它试图找到匹配的括号。因为使用 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

分享的分数的最后一个片段是使用你的选择来制作分数的片段。你可以先选择一些文字然后按Tab,键入/并再按Tab一次来使用它。

visualfrac-0da01b9566dc437fabf0c563ed14941f.gif

代码使用${VISUAL}代表您选择的变量。

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

Sympy和Mathematica

另一个很酷但很少使用的片段是使用 sympy 来评估数学表达式的片段。例如:sympy Tab展开到 sympy | sympy,然后 sympy 1 + 1 sympy Tab 展开到 2。

sympy-8c8f143fdcdc2f3e6de9853cf7ea3def.gif

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 用户,你可以做类似的事情:

mathematica-82f7f187a685a52ed7665fded563f936.gif

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

Postfix片段

我认为值得分享的其他一些代码段是 postfix 代码段。这种片段的例子是 phat→ \hat{p} 和 zbar→ \overline{z}。类似的片段是后缀矢量,例如 v,.→ \vec{v} 和 v.,→ \vec{v}。顺序,和.无关紧要,所以我可以同时按下它们。这些片段可以节省时间,因为您可以输入讲师在黑板上写的相同顺序。

barhatvec-7c2e62aae292bc4d65eae363c816e206.gif

请注意,我仍然可以使用 bar 和 hat 前缀,因为我已经添加了较低的优先级。这些代码段的代码是:

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

其他片段

我有大约100个其他常用的片段。它们在 这里 可用。其中大多数都很简单。例如,!> 成为 \mapsto,->成为 \to 等

complex5-4ee36ef41c17f1ded1fffe13ba32b60d.gif

fun变成f: \R \to \R :,!>→ \mapsto,->→ \to,cc→ \subset。

fun3-a45092c95d91059b779f74cedda840e1.gif

lim变成\lim_{n \to \infty},sum→ \sum_{n = 1}^{\infty},ooo→\infty

sum4-2be9c99e6ab5787fc5196bbf4ce525a7.gif

bazel2-3be522f5e6e5ca7f871138732ba1b14d.gif

课程特定的片段

除了我常用的片段,我还有特定课程的片段。这些是通过将以下内容添加到我的.vimrc:

set rtp+=~/current_course

哪里 current_course 是我目前激活的课程的符号链接(更多关于在另一篇博文中的内容)。在该文件夹中,我有一个文件 ~/current_course/UltiSnips/tex.snippets,其中包含课程特定的片段。例如,对于量子力学,我有 bra / ket 表示法的片段。




<a|\bra{a}
<q|\bra{\psi}
|a>\ket{a}
|q>\ket{\psi}
<a|b>\braket{a}{b}

正如 \psi 在量子力学中经常使用的那样,我在扩展时替换 q 了 braket 中的所有实例 \psi。

braket-268d331fdb83b6fa3563b6f2b0baac66.gif

snippet "\<(.*?)\|" "bra" riA
\bra{`!p snip.rv = match.group(1).replace('q', f'\psi').replace('f', f'\phi')`}
endsnippet

snippet "\|(.*?)\>" "ket" riA
\ket{`!p snip.rv = match.group(1).replace('q', f'\psi').replace('f', f'\phi')`}
endsnippet

snippet "(.*)\\bra{(.*?)}([^\|]*?)\>" "braket" riA
`!p snip.rv = match.group(1)`\braket{`!p snip.rv = match.group(2)`}{`!p snip.rv = match.group(3).replace('q', f'\psi').replace('f', f'\phi')`}
endsnippet

上下文

在编写这些片段时需要考虑的一件事是,“这些片段会与长与常用的文本冲突吗?”

例如,在英语中大约有72个单词包含sr,这意味着当输入disregard这个词时,sr会扩展到^2,出现一个di^2egard。

这个问题的解决方案是,为代码片段添加上下文。

通过使用 Vim 的语法突出显示,可以确定UltiSnips是否应该扩展片段,这取决于你使用的是数学还是文本。

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()"到仅在数学上下文中展开的片段。

context "math()"
snippet sr "^2" iA
^2
endsnippet

请注意,“数学上下文”是一个微妙的事情。有时您可以使用在数学环境中添加一些文本 \text{...}。在这种情况下,您不希望片段扩展。但是,在以下情况中:\[ \text{$...$} \] 它们应该扩展。这就是 math 上下文代码有点复杂的原因。以下动画说明了这些细微之处。

syntaxtree-e1b3ca2dd4fc1f5fe39679fb6ba38aad.gif

结论

在 Vim 中使用片段,编写 LaTeX 不再是烦恼,而是一种乐趣,结合动态拼写检查,它可以实现舒适的数学笔记设置。但是缺少一些部分,例如以数字方式绘制图形并将它们嵌入到 LaTeX 文档中。

相关链接资源,工具传送门:

Linux和Mac系统自带Vim。

Windows版本Vim:
https://ftp.nluug.nl/pub/vim/pc/gvim81.exe 

Vim插件管理:
https://github.com/junegunn/vim-plug 

Vim LaTeX插件:
https://github.com/lervag/vimtex 

窗口平铺管理器:
https://github.com/baskerville/bspwm 

管理Vim片段工具:
https://github.com/SirVer/ultisnips 

如果你用不惯Vim,还有Emacs、Atom、VS Code、Sublime,它们都有LaTeX插件,总有一款文本编辑器适合你。想要熟悉更多的LaTeX使用方法,就需要系统地学习,平时多加练习也必不可少。

点赞(26)

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部