數學(xué)課上,全程鍵盤(pán)手打1700頁(yè)筆記。
速度緊追老師板書(shū),公式、圖形一個(gè)不落。
效果?請看下圖:
不僅排版媲美教科書(shū),而且還能夠批注,檢索關(guān)鍵詞……
筆記被他Po到網(wǎng)上之后,便引來(lái)大量圍觀(guān)。
不到一天,相關(guān)推文就已經(jīng)有2000多贊,Hacker News論壇上蓋了200多樓。
甚至有網(wǎng)友評論稱(chēng):“你就是我們需要的英雄!”
他是怎么做到的呢?秘密武器就是:LaTeX+Vim!
這位來(lái)自歐洲的小哥非常強烈安利Vim文本編輯器,他說(shuō):
用LaTeX寫(xiě)數學(xué)公式,我選Vim編輯器。它強大、通用、可擴展性很強。只要是基于文本的任務(wù)我都用它,寫(xiě)代碼、編輯LaTeX、寫(xiě)markdown都是。
雖然入門(mén)階段的學(xué)習曲線(xiàn)超級陡峭,但只要掌握了基本的操作方式,就會(huì )欲罷不能。
下面就讓我們看一下他完成這一壯舉的具體流程,文中提到的工具下載地址,我們都附在了最后。
我們先看看小哥的工作環(huán)境配置。
他用Vim編輯LaTeX的場(chǎng)景,就像下面這樣:
左邊是Vim,右邊是pdf閱讀器Zathura,它也有類(lèi)似Vim的快捷鍵。
小哥用的操作系統是Ubuntu,使用bspwm作為窗口管理器。在Vim中,使用的LaTex插件是vimtex,它有語(yǔ)法高亮顯示、目錄視圖、同步對象等功能。
然后,使用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'
最后兩行控制的是“隱藏”功能。開(kāi)啟了這個(gè)功能,除了你光標所在的那一行之外,文本里夾雜的LaTeX代碼就都會(huì )隱藏或者替換成其他符號。
比如說(shuō)在下面動(dòng)圖里,隱藏了[,],$之后,沒(méi)有了它們的干擾,整個(gè)文檔就更易讀。這個(gè)功能還會(huì )用∩替代\bigcap,∈替代\in等等。
設置完成,接下來(lái)就到了整個(gè)教程的精華所在:
用LaTeX記筆記,怎么才能像老師寫(xiě)板書(shū)一樣快?
這就是片段(snippets)發(fā)揮作用地方了。
片段是什么?
片段是一小段可復用的文本,由其他文本觸發(fā)。
例如,輸入sign,再按下Tab鍵,這個(gè)單詞就會(huì )自動(dòng)擴展為一段簽名:
片段也可以是動(dòng)態(tài)的:輸入today并按下Tab鍵,它就會(huì )變成當前的日期。

而輸入box按Tab,就會(huì )出現一個(gè)框,還會(huì )隨著(zhù)輸入文字自動(dòng)變大。

片段,甚至可以嵌套在另一個(gè)片段里用:

怎么創(chuàng )建片段?使用UltiSnips
管理片段的插件UltiSnips,小哥是這樣配置的:
Plug 'sirver/ultisnips'
let g:UltiSnipsExpandTrigger = '<tab>'
let g:UltiSnipsJumpForwardTrigger = '<tab>'
let g:UltiSnipsJumpBackwardTrigger = '<s-tab>'
關(guān)于sign片段的代碼如下:
snippet sign 'Signature'
Yours sincerely,
Gilles Castel
endsnippet
對于動(dòng)態(tài)的片段,你可以將代碼放在``之間, 在片段擴展的時(shí)候,就會(huì )運行。下面的例子,就是用 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 的值替換。在這些代碼塊中,你可以訪(fǎng)問(wèn)代碼段的當前狀態(tài),例如t[1]包含第一個(gè)制表位,fn是當前文件名等等。
使用片段編寫(xiě)LaTeX,要比純手工編寫(xiě)快得多。特別有些非常復雜的片段能幫你大大節約時(shí)間,有效防止抓狂。
下面是一些非常有用且容易上手的片段:
環(huán)境
想插入一個(gè)環(huán)境,只需要在一行的開(kāi)頭輸入beg。然后鍵入環(huán)境的名稱(chēng),這個(gè)名稱(chēng)在\end{}命令中也是一樣。按下Tab鍵,就能夠將光標放置在新創(chuàng )建的環(huán)境中。

這個(gè)片段的代碼如下:
snippet beg 'begin{} / end{}' bA
\begin{$1}
$0
\end{$1}
endsnippet其中,b表示這個(gè)片段只會(huì )在代碼行的開(kāi)頭展開(kāi),A代表自動(dòng)展開(kāi),也就是說(shuō)不用按Tab鍵了。制表位(Tab stop)——也就是你可以通過(guò)按Tab 和Shift+Tab跳轉到的位置——用$1、 $2、......來(lái)表示,最后一個(gè)用$0。
行內和數學(xué)顯示
在記數學(xué)筆記的過(guò)程中,最常用的兩個(gè)片段是mk和dm。
它們負責啟動(dòng)數學(xué)模式。第一個(gè)片段用于“行內數學(xué)”,第二個(gè)用于“顯示數學(xué)”。

代碼行內的數學(xué)片段是“智能的”:它知道什么時(shí)候在$符號后面直接輸入一個(gè)單詞,它會(huì )自動(dòng)加個(gè)空格。但如果輸入一個(gè)非單詞的字符,它就不會(huì )添加空格了,比如在““$p$-value”情況下,是這樣的:

這個(gè)片段的代碼如下:
snippet mk 'Math' wA
$${1}$`!p
if t[2] and t[2][0] not in [',', '.', '?', '-', ' ']:
snip.rv = ' '
else:
snip.rv = ''
`$2
endsnippet第一行末尾的w,意味著(zhù)這個(gè)片段會(huì )在單詞邊界處擴展,例如,hellomk不會(huì )擴展,但是hello mk會(huì )。
用于顯示數學(xué)的片段更簡(jiǎn)單,也更加方便;有了它,你可能再也不會(huì )忘記用句號結束方程了。

代碼:
snippet dm 'Math' wA
\[
$1
.\] $0
endsnippet小寫(xiě)和上標
另一個(gè)很有用的片段就是下標。能夠把a1改為a1,把a_12改為a{12}。

這個(gè)片段的觸發(fā)器是使用正則表達式。有兩種情況會(huì )擴展片段。一是你鍵入一個(gè)字符,后面跟著(zhù)一個(gè)數字,比如[A-Za-z]\d;另一種是,一個(gè)字符后面有并跟著(zhù)兩個(gè)數字,比如[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
當你使用括號將正則表達式的一部分裝在一個(gè)組中時(shí),例如(\d\d),你可以在 Python中通過(guò)match.group (i)來(lái)使用它們擴展片段。
至于上標,可以使用td,它就會(huì )變成^{}。然而,對于平方、立方和其他一些常見(jiàn)的片段,可以使用專(zhuān)門(mén)的代碼片段,如 sr、cb等等。
效果圖:

代碼:
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分數
分數是一個(gè)用起來(lái)最方便的一個(gè)片段,擴展的形式如下:
/ / → 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)}{

第一個(gè)片段的代碼很簡(jiǎn)單:
snippet // 'Fraction' iA
\\frac{$1}{$2}$0
endsnippet第二個(gè)和第三個(gè)示例,可以使用正則表達式來(lái)匹配3/、4ac/、6pi^2/、a2/等表達式。
snippet '((\d+)|(\d*)(\\)?([A-Za-z]+)((\^|_)(\{\d+\}|\d))*)/' 'Fraction' wrA
\\frac{`!p snip.rv = match.group(1)`}{$1}$0
endsnippet
看了上邊這些,你可能覺(jué)得正則表達式太難了。沒(méi)關(guān)系,下面有一個(gè)解釋得非常直觀(guān)的圖表:

在第四和第五種示例下,要換一種方法。使用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這里最后要分享的關(guān)于分數的片段,能根據你的選擇,來(lái)生成一個(gè)分數。
你可以先選擇一些文本,然后按Tab鍵,繼續輸入、然后再按Tab鍵。

代碼中,使用${VISUAL}變量來(lái)表示所選的內容。
snippet / 'Fraction' iA
\\frac{${VISUAL}}{$1}$0
endsnippet
Sympy和Mathematica
還有一個(gè)很酷但用得不多的片段,是使用Sympy來(lái)計算數學(xué)表達式。例如,輸入sympy,然后按下Tab,可以擴展為sympy | sympy,輸入sympy 1 + 1 sympy,按下Tab,可以擴展為2。

片段代碼:
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,也可以做類(lèi)似的事情:

片段代碼:
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}。還有類(lèi)似的后綴向量,例如v,.→vec{v}和v.,→vec{v}。.和,的順序沒(méi)關(guān)系,所以可以同時(shí)按下它們兩個(gè)。

這些片段真的可以節省時(shí)間,可以按照和老師寫(xiě)板書(shū)一樣的順序來(lái)記。
注意,bar和hat前綴也依然可以用,只要以較低的優(yōu)先級添加它們就行。
這些片段的代碼是:
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個(gè)常用的片段(下載地址附于文末),大多數都很簡(jiǎn)單。比如,輸入!>變成\mapsto,輸入->變成\to等等。

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

lim變成\lim{n \to \infty},sum變成\sum{n = 1}^{\infty},ooo變成\infty。


特定課程的片段
除了一些常用的片段,也可以針對特定的課程設定片段。例如,在量子力學(xué)這門(mén)課中,可以設定一些關(guān)于bra/ket符號的片段。
<a|→\bra{a} <ψ|→\bra{\psi}='' a=''>→\ket{a}
|ψ>→\ket{\psi}

代碼:
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
上下文
在編寫(xiě)這些片段時(shí)需要考慮的一件事是,“這些片段會(huì )與長(cháng)與常用的文本沖突嗎?”
例如,在英語(yǔ)中大約有72個(gè)單詞包含sr,這意味著(zhù)當輸入disregard這個(gè)詞時(shí),sr會(huì )擴展到^2,出現一個(gè)di^2egard。
這個(gè)問(wèn)題的解決方案是,為代碼片段添加上下文。
通過(guò)使用 Vim 的語(yǔ)法突出顯示,可以確定UltiSnips是否應該擴展片段,這取決于你使用的是數學(xué)還是文本。
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()”添加到只希望在數學(xué)上下文中展開(kāi)的片段中。
context 'math()'
snippet sr '^2' iA
^2
endsnippet請注意,“數學(xué)上下文”是一個(gè)微妙的東西。 有時(shí)你可以使用\text{…}在數學(xué)環(huán)境中添加一些文本。在這種情況下,你不需要擴展片段。但是,在以下情況下: \[ \text{$...$} \],它們可以擴展。 這就是為什么math上下文的代碼有點(diǎn)復雜。下面的動(dòng)圖說(shuō)明了這些微妙之處。

除了上述一些片段,你也可以根據自己的需要,來(lái)自己添加一些插件或者片段,來(lái)提高自己的效率。
純手打記下1700頁(yè)數學(xué)筆記,awesome都不夠形容了這位小哥了,堪稱(chēng)理工科學(xué)生中的“英雄”。

并非所有人都贊同小哥的做法,強大的高科技工具在傳統面前常常會(huì )被質(zhì)疑。
有部分網(wǎng)友認為手寫(xiě)比電腦打字印象深刻,而且要達到這位小哥的熟練程度,恐怕LaTeX和Vim得練習好幾年。
既然用筆更方便,為什么還要用電腦來(lái)記筆記呢?原因很簡(jiǎn)單:字太丑!

如果記下來(lái)的內容連自己看的欲望都沒(méi)有,怎么復習課堂筆記呢?至少用電腦記下來(lái)的排版工整,讓人賞心悅目。
雖然國外網(wǎng)友爭論不休,但在國內只要一個(gè)條件就可以徹底否決這個(gè)方法:不讓帶電腦進(jìn)課堂。
對此,你怎么看?
Linux和Mac系統自帶Vim。
Windows用戶(hù)安裝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常見(jiàn)數學(xué)符號輸入方法:
https://en.wikibooks.org/wiki/LaTeX/Mathematics
想要熟悉更多的LaTeX使用方法,就需要系統地學(xué)習,平時(shí)多加練習也必不可少。
博文鏈接:
https://castel.dev/post/lecture-notes-1/
作者系網(wǎng)易新聞·網(wǎng)易號“各有態(tài)度”簽約作者
— 完 —
訂閱AI內參,獲取AI行業(yè)資訊
加入社群
量子位AI社群開(kāi)始招募啦,量子位社群分:AI討論群、AI+行業(yè)群、AI技術(shù)群;
聯(lián)系客服