世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的

Posted 机器之心

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的相关的知识,希望对你有一定的参考价值。

选自 castel.dev

机器之心编译


一般你是用手写还是 MarkDown 做数学笔记?在这篇文章中,作者介绍了如何用 LaTex 和 Vim 实时做数学笔记,通过一系列炫酷的技巧,不论是表达式板书还是图像绘制,我们都能实时跟得上。


在的学习过程中,很多时候都需要手动推导或最优化过程。这些推导很可能是老师在黑板上一边写一边解释,有时候我们会仔细听解释,并顾不上记笔记与注解。因此课程音频与视频就很重要了。但如果我们能跟得上老师的手写速度,那么以后回忆这些笔记就方便很多。


例如在最近结课的 CS224n 2019 中,Christopher Manning 在第一节课就手动推 Word2Vec 的最优化与权重更新过程。由此可见,飞一般地记数学笔记还是很有必要的。


图片截图来自 CS224n 2019


在这篇文章中,作者在攻读数学专业学士学位的第二个学期开始用 LaTex 做课堂笔记,自此以后一直在使用,总共记下了 1700 多页的笔记。以下是一些例子,你可以看看用 LaTex 做出笔记是什么样子的。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的

世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的

世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的



这些包括图表在内的笔记,都是在上课期间完成的,之后没有修订过。为了使 LaTex 做笔记可行,作者在头脑中设下以下四个目标:


  • 利用 LaTex 记文本和数学公式应与讲师在黑板上写字的速度保持一致:不允许延误。

  • 画图的速度应尽量与讲师保持一致。

  • 做笔记,如添加注解、编辑所有注解、整合最后两堂课的内容、搜索注解等,应方便快捷。

  • 当我们想在 pdf 文件旁边添加注释时,利用 LaTex 应能够实现这一目的。


以下从 Vim+LaTex 到 Snip­pets,作者介绍了如何科学地记数学笔记。


Vim 和 LaTex


我使用 Vim 在 LaTex 中记文本和数学公式。Vim 是一个功能强大的通用文本编辑器,可扩展性很强。我使用 Vim 写代码、LaTex、mark­down 等一切基于文本的东西。Vim 具有一个非常陡峭的学习曲线,一旦你弄清楚了基本原理,则很难再使用那些缺少 Vim 快捷键绑定的编辑器。以下是我编辑 LaTex 时屏幕的样子:


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


左边是 Vim,右边是我的 pdf 查看器 Zathura(也有类似于 Vim 的快捷键绑定)。我正使用带有 bspwm 的 Ubuntu 作为自己的窗口管理器。我在 Vim 使用的 LaTex 插件是 vimtex。该插件提供句法高亮显示、内容图表以及 synctex 等。使用 vim 插件的设置如下所示:


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 代码会被替代或隐藏。通过隐藏 [、] 和$等标志符,你可以更舒服地浏览文件。这一特征也以∩替代igcap,以∈替代in 等。以下动画应能够使这一过程更清楚。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


这篇博客要解决的核心问题是:用 LaTex 做笔记如何与讲师在黑板上写字的速度保持一致。这都要归功于 snippet。


Snippets


一个 snippet 是一段可重复使用的短文本,可用来编辑其他文本。例如,当我键入 sign 并按下 Tab 时,单词 sign 将会补全为一个自定义的签名。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


Snippet 也可以是动态的:当我键入 today 并按下 Tab 时,单词 today 将会被当前日期替代;键入 box Tab 变成一个可以自动增大的框。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的

世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


你甚至可以在另一个 snippet 中使用 snippet。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


利用 UltiSnip 创建 Snippet


我使用插件 UltiSnip 来管理我的 snippet,其设置如下:


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


定义 sign 的 snippet 的代码如下所示:


snippet sign "Signature"
Yours sincerely,

Gilles Castel
endsnippet


对于动态 snippet,你可以在倒引号``之间输入代码,并在 snippet 扩展时运行。我在这里使用 bash 编译器格式化当前日期:date + %F。


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


你也可以在`!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 表示当前文档名称。


LaTeX snippet


借助于 snippet,利用 LaTeX 做笔记要比手写快得多。一些复杂的 snippet 可以为你节省大量时间并消除做笔记的挫败感。让我们开始了解一些简单的 snippet。


环境


为了嵌入环境,我必须在一行开端键入 beg。之后我键入环境名称,后者会直接在end{} 指令中映出。按下 Tab 使光标位于新创建的环境中。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


该 snippet 的代码如下所示:


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


b 意味着该 snippet 将只能在一行开端扩展,并且 A 代表自动扩展,这意味着我不需要键入 Tab 就可以扩展 snippet。制表位--即可以通过按下 Tab 和 Shift+Tab 跳转到的地方--以$1、$2 等表示,同时最后一个为$0。


inline math 和 display math


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


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


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


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


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


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


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


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


上下标


另一个有用的 snippet 主要针对下标,该 snippet 自动将 a1 更改为 a_1 以及 a_12 更改为 a_{12}。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


该 snippet 代码使用正则表达式作为触发器。当你在 [A-Za-z]d 编码的数字后面键入一个字符,或者在 _以及两个数字 [A-Za-z]_dd 后面键入一个字符时,触发器会扩展该 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])_(dd)' "auto subscript2" wrA
`!p snip.rv = match.group(1)`_{`!p snip.rv = match.group(2)`}
endsnippet


当你在使用圆括弧包装部分正则表达式时,如 (dd),你可以通过 Python 中的 match.group(i) 在扩展 snippet 中使用它们。


对于上标而言,我使用 td 自动扩展为 ^{} 的。但是,对于平方、立方、以及一小部分其他常见上标,我使用 sr、cb、comp 等专门的 snippet。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


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:1700多页数学笔记是如何实时完成的

世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


第一个的代码非常简单:


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


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


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


如你所见,正则表达式可能非常复杂,这里有个图表可以解释:


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


在第四、第五个例子中,它试图找到匹配圆括弧。由于不能使用 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 == 0break;
    i -= 1
snip.rv = stripped[0:i] + "\frac{" + stripped[i+1:-1] + "}"
`{$1}$0
endsnippet


最后一个与分数有关的 snippet 即使用你的选择来制作分数。你可以先用它选择一些文本,然后按下 Tab,打出/,然后再按下 Tab。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


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


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


sympy 和 Math­e­mat­i­ca


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


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


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


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


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


后缀 snippet


值得分享的还有后缀 snippet,如 phat → hat{p},zbar → overline{z} 等。一个类似的 snippet 是后缀向量,如 v → vec{v}。这些 snippet 真的非常节省时间,因为你可以借此跟上老师板书的节奏。


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


注意,我还是可以用 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


我还有 100 多个其他常用的 snippet,其中多数非常简单。例如,!>变成 mapsto,->变成 o 等。



世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


上下文管理


写这些 snip­pet 的时候需要考虑:这些 snippet 和普通文本有冲突吗?例如,我的词典里有大约 72 个英文单词、2000 个包含 sr 的荷兰语单词,也就是说,如果我打出 disregard,sr 就会变成^2,我会得到 di^2egard。


解决方案是在 snippet 中加入一个上下文管理文(Con­text)管理方法。使用 Vim 的句法高亮,就可以根据你是在写数学还是文本来决定 Ul­tiSnips 是否应该扩展 snippet。我的想法如下:


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 了。


注意,「数学上下文」这个说法也很微妙。有时候你通过使用 ext{...} 将一些文本添加到数学环境中。在那种情况下,你不想让 snip­pet 扩展。然而,在[ ext{$...$} ] 中,你又需要扩展。所以「数学上下文」这个说法有点不好界定,如下图所示:


世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的


实时纠正拼写错误


尽管学习数学是我做笔记的一个重要部分,但大部分时间我都在打英语单词。我的打字技术还不错,每分钟 80 词左右,但我还是会时不时地出错。所以我在 Vim 上添加了快捷键绑定,纠正拼写错误,以免打断我的工作流程。我按下 Ctrl+L 键就可以纠正之前的拼写错误,就像这样:



我的拼写检查设置如下:


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


它跳转到之前的拼写错误 [s,然后选取第一个建议 1z=,接下来跳回 `]a。中间的<c-g>u 使得快速纠正拼写错误成为可能。


结论


使用 Vim 中的 snip­pet 使得书写 LaTeX 不再那么头疼,反而成为一种享受。与实时拼写检查结合之后,记数学笔记变得非常舒服。后续博文将讨论数字绘图及将图嵌入 LaTex 文本等内容。虽然前期学习成本会有一些,但熟悉后板书推导就能飞一般地记载。


原文链接:https://castel.dev/post/lecture-notes-1/



✄------------------------------------------------

加入机器之心(全职记者 / 实习生):hr@jiqizhixin.com

投稿或寻求报道:content@jiqizhixin.com

广告 & 商务合作:bd@jiqizhixin.com

以上是关于世界上最好的编辑器Vim:1700多页数学笔记是如何实时完成的的主要内容,如果未能解决你的问题,请参考以下文章

Linux学习笔记——文件/目录/VIM

史上最全的Vim命令

学习笔记——Liunx;Linux文件与目录结构;VI/VIM编辑器(一般模式编辑模式命令模式)

Linux学习之十三-vi和vim编辑器及其快捷键

开发Python用啥工具好

配置Vim编辑器