用 Vim 和 LaTeX 在数学课上跟上授课速度并记下笔记(一)

前言

vim + LaTeX 一直是 Geek 的必备。

译者:Bon

原作者:Gilles Castel

声明:本次翻译已经获得原作者的授权

Disclaimer:This translation is permitted by Gilles Castel

原博客链接:How I’m able to take notes in mathematics lectures using LaTeX and Vim | Gilles Castel

前一段时间我在 Quora 上回答了一个问题: Can peo­ple ac­tu­al­ly keep up with note-taking in Math­e­mat­ics lec­tures with LaTeX(人是否真的可以用 LaTeX 跟上课程的速度来记笔记)。在那里,我解释了我的 Vim 和 LaTeX 的工作流以及我是如何在 Inkscape 中进行绘图的。但是工作流从那以后(译注:2017年)变化了许多, 我想写一些博客文章来解释我现在的工作流程。

我在我本科二年级开始使用 LaTeX 来进行笔记的记录,然后我从那以后也一直在使用它,下边 是总共1700页笔记中的一小部分:

LaTex-1

LaTeX-2

LaTeX-3

这些课程笔记——包括示意图——都是我在上课的时候做的,而且在课后完全没有编辑过。 为了保证让自己用 LaTeX 记录笔记的时候的可行性,我给自己设立了四个目标:

  • 在 LaTeX 中编写文本和数字公式的时候我应该可以与讲师在黑板上书写一样的快,不允许有任何延迟。
  • 画示意图应该几乎和讲师一样的快。
  • 管理笔记,例如,添加笔记、编译所有的笔记、编译最新的两个课程笔记、搜索笔记都应该尽可能的简单和快速。
  • 当我想在 pdf 文档旁边进行标注的时候,我应该可以使用 LaTeX 的方法来标注文档。

这篇博客将会重点介绍第一个方案:如何编写 LaTeX

Vim and LaTeX

为了可以在 LaTeX 中编写文本和数学公式,我用的是 Vim。Vim 是一个功能强大的通用文本编辑器,具有很强的可拓展性。我用它来编写代码、LaTeX、markdown,……,等一切基于文本的东西。虽说它具有一个很陡峭的学习曲线,但是一旦你掌握了基础的知识,就很难回到没有 Vim 的编辑器。这是我编辑 LaTeX 的时候屏幕的样子:

vim-zathura

在左边你看到的是 Vim ,在右边你看到的是我的 pdf 阅读器, Za­thu­ra,这是一款具有 Vim 键绑定的 pdf 阅读器。 我用 bspwm 来作为我的 Ubuntu 系统的窗口管理。我在 Vim 中使用的 LaTeX 插件叫做 vim­tex ,它提供了语法高亮、目录预览、自动编译 Tex 文件的特性。此外,我用的插件管理器是 vim-plug,下边是我的简单的配置:

1
2
3
4
5
6
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 代码中的隐藏。 这是一个替换 LaTeX 代码来让其不可见的功能(译注:在这种模式中,当你用的是 INSERT 模式的时候,还是会看得到,这里指的是在VISIBLE 模式)。通过使\[, \], $ 之类的不可见,你的文档不会因为有它们的存在而变得那么突兀。这个功能也可以使 \bigcap 被替换成 , \in 被替换成 等。在下边的动画中,你可以清晰地看到。

conceal

在已经进行了这个设置的前提下,我们就来到这篇博客的关键了:与讲师在黑板上书写一样的快地在 LaTeX 中编写文本和数字公式。这是因为有 Snippets(片段)发挥了它们强大的作用。

注意:从这里开始下文 snippet 都会直接翻译成片段


片段

什么是片段 ?

片段是一种可以由其它的文本触发的可重复使用的短文本。例如,当我键入sign 然后按下Tab 键,单词 sign 就会拓展成一个签名:

sign

片段也可以是动态的:当我键入 today 然后按下Tab, 单词 today 就会被当前的日期给取代,还有如果输入 box 然后 Tab 一下就会变成一个自适应的框:

today

box

你也可以在一个片段中使用另外的片段:

todaybox

使用 Ul­tiSnips 来创建片段

我使用 Ul­tiSnips 来管理我的片段。我的配置如下:

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

sign 的片段在后边:

1
2
3
4
5
snippet sign "Signature"
Yours sincerely,

Gilles Castel
endsnippet

对于想要动态的片段,你可以把代码放在反引号

`之间,而这个会在片段展开的时候自动运行。在这里,我使用了 Bash 的代码风格:`date + %F`.
1
2
3
4
5

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

你也可以在 !p … 块内使用 Python。看一下 box 片段中的代码:

1
2
3
4
5
6
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 会比手动编写快得多,特别是一些更复杂的片段,而这就可以为你节约特别多的时间以及让你远离失败的挫折。让我们从编写一些简单的片段开始。

环境(Environment)

想要插入一个环境,我所需要做的就只有在行开头的地方键入 beg,然后我就只需要直接来输入环境的名字了。而这个同样会在 \end{} 行中输入。按下 Tab会让光标自动在环境之中出现。

beginend

而这个片段的代码如下。

1
2
3
4
5
snippet beg "begin{} / end{}" bA
\begin{$1}
$0
\end{$1}
endsnippet

b 表示这个片段只会在行开头展开,而 A 表示这自动展开,而这意味着我不需要再次按下Tab键就可以自动展开了。 制表符——也就是说你可以通过按Tab 或者 Tab+Shift 来跳转。而Tab$1, $2,…… 最后一个用$0来表示。

行内或者块显示数学

我最常用的两个片段是 mkdm。它们是负责启动数学模式的片段。 第一个指的是行内数学公式的片段,而第二个是显示块内数学公式的片段。

mkdm

而行内数学的片段是”聪明的“: 他会知道何时在 $ 符号后插入空格。当我直接输入一个单词后和 $后直接结束,它会添加一个空格。 然而,当我键入一个非单词字符,它不会添加空格,例如在$p$-value 的情况下。

mk

此片段的代码如下。

1
2
3
4
5
6
7
8
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

1
2
3
4
5
snippet dm "Math" wA
\[
$1
.\] $0
endsnippet

上下标

另一个有用的片段是下标。它会自动将 a1 改成 a_1 ,然后 a_12 会被改成 a_{12}

subscripts

片段的代码用正则表达式来作为触发器。它会在你键入一个字符 [A-Za-z]\d,而且后边还带着数字字符的时候,或者字符 [A-Za-z]_\d\d带着 _ 和两个数字的时候会自动展开:

1
2
3
4
5
6
7
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) 来在扩展代码段中使用它.

而对于上标, 我会使用 td,这个会扩展成 ^{}。然而,对于平方、立方或者其它比较常见的。我使用专门的片段,例如 sr, cbcomp

superscripts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

第一个的代码很简单:

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

在第二还有第三个例子中,我是使用正则表达式来匹配 3/, 4ac/, 6\pi^2/, a_2/等的。

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

正如你所看到的,正则表达式可能会特别的庞大,下边是一个解释它的图例:

regex

在第四和第五个例子中,它会试图找到想要匹配的括号,而这个靠 Ul­tiSnips 的正则表达式引擎是不可能的,所以我用 Python 来实现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

代码使用 ${VISUAL} 变量来指代你的选择。

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

Sympy 和 Math­e­mat­i­ca

另外一个很酷但是很少用到的片段是使用 sympy 来得到数学表达式的结果的片段。例如:键入 sympy 然后 Tab 来展开成 sympy | sympy,然后 sympy 1 + 1 sympy 展开成 2.

sympy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

对于 Math­e­mat­i­ca 的用户,你也可以做到这样的效果:

mathematica

1
2
3
4
5
6
7
8
9
10
11
12
13
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

后缀片段

还有一些我觉得可以分享的片段就是后缀片段。关于这种片段的一些例子有 phat\hat{p} 以及 zbar\overline{z}。一个类似的片段就是后缀矢量,例如 v,.\vec{v} 还有 v.,\vec{v}. 而 ,. 的先后顺序并不会有什么影响,所以我可以同时按下他们两个。 这些片段可以大大地节省你的时间,因为你可以和讲师在黑板上写的顺序相同地键入。

barhatvec

请注意,我可以同时使用 bar and hat 前缀,因为我已经添加了较低的优先级。 而关于这个片段的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 个其它的常用片段。你可以在 这里 (译注:除此以外,我存了一份在百度云上链接: s/1QgFJckgzuGBZmBiNaEMWRg code: gyhb)获取。例如, !> 会展开成 \mapsto-> 展开成 \to等。

complex5

fun 会展开成 f: \R \to \R :, !>\mapsto, ->\to, cc\subset.

fun3

lim 展开成 \lim_{n \to \infty}sum\sum_{n = 1}^{\infty}ooo\infty

sum4

bazel2

课程特定的片段

除了我一些常用的片段,我也会使用课程常用的片段。这些会被我加入我特定的 .vimrc:

1
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}
`<ab>`\braket{a}{b}

正如 \psi 会在量子力学中经常使用,我在展开的时候替换了所有的 q\psi{}

braket

1
2
3
4
5
6
7
8
9
10
11
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 个英语单词还有 2000 个德语单词会包括 sr 的。这个就意味着当我输入 disregard的时候, sr 会展开成 ^2,然后会得到 di^2egard

而这个的解决方案就是给这个片段添加上下文。利用 Vim 的语法高亮,它可以来决定 Ul­tiSnips 是否需要展开这个片段,而且是会基于你是在写数学公式还是说你在输入文本。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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()" 到你只想在数学的上下文中展开的代码段中去。

1
2
3
4
context "math()"
snippet sr "^2" iA
^2
endsnippet

请注意,”数学上下文” 有点微妙。有时候你可能想要在数学环境中添加 \text{…},而这种情况中你不会想要片段会自动扩展。但是在下边的情况中, \[ \text{$...$} \],它们又应该扩展。这就是 math 的代码的上下文会有点复杂的原因。以下的 Gif 图展示了这些细微之处。

syntaxtree

在你飞速编写的过程中纠正拼写错误

虽然插入数学公式是我设置我的笔记键入方式的重要部分,但是我大部分时间都在输入英文。而且我每分钟大约有 80 个单词。尽管我的打字技巧也不错,但是我还是会犯不少的错误。 这就是为什么,我要给 Vim 绑定一个键来纠正我耳朵拼写错误,而且这个过程不会中断我的工作流。当我键入 Ctrl+L 的时候,我之前的拼写错误都会被纠正。它看起来就好像这样:

spellcheck

我的拼写检查的设置如下:

1
2
3
setlocal spell
set spelllang=nl,en_gb
inoremap <C-l> <c-g>u<Esc>[s1z=`]a<c-g>u

它基本上会跳到最前边的拼写错误 [s,然后选择第一个建议 1z=,然后跳回到 ]a。而 在中间的u` 可以保证快速解决掉拼写校正。

总结

在 Vim 中使用片段,编写 LaTeX 再也不会是一种烦恼,相反的,是一种乐趣。结合动态的拼写检查,它可以实现舒适的数学笔记设置。但是缺少了一些部分,例如怎么用数字的方式来绘制图形,然后将它嵌入到 LaTeX 文档中去。这是我未来想要在博客文章中讨论的问题。

喜欢这篇博客?分享出去!

本文作者: Bon
本文地址https://bonxg.com/p/85.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!

# Bon
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×