← All articlesMar 16, 2019 18 Min Read

How I'm able to take notes in mathematics lectures using LaTeX and Vim

A while back I an­swered a ques­tion on Quora: Can peo­ple ac­tu­al­ly keep up with note-​taking in Math­e­mat­ics lec­tures with LaTeX. There, I ex­plained my work­flow of tak­ing lec­ture notes in LaTeX using Vim and how I draw fig­ures in Inkscape. How­ev­er, a lot has changed since then and I’d like to write a few blog posts ex­plain­ing my work­flow.

I start­ed using LaTeX to write lec­ture notes in the sec­ond se­mes­ter of my bach­e­lor in math­e­mat­ics, and I’ve been using it ever since, which makes for a total of more than 1700 pages of notes. To give you an idea of what those notes look like, here are some ex­am­ples:

ca1 ca2 ca3

These lec­ture notes — in­clud­ing fig­ures — are made while at­tend­ing the lec­ture and have not been edit­ed af­ter­wards. To make note tak­ing using LaTeX vi­able, I had four goals in mind:

  • Writ­ing text and math­e­mat­i­cal for­mu­las in LaTeX should be as fast as the lec­tur­er writ­ing on a black­board: no delay is ac­cept­able.
  • Draw­ing fig­ures should be al­most as fast as the lec­tur­er.
  • Man­ag­ing notes, i.e. adding a note, com­pil­ing all my notes, com­pil­ing the last two lec­tures, search­ing in notes, etc. should be easy and quick.
  • An­no­tat­ing pdf doc­u­ments using LaTeX should be pos­si­ble for when I want to write notes along­side a pdf doc­u­ment.

This blog post will focus on the first item: writ­ing LaTeX.

Vim and LaTeX

For writ­ing text and math­e­mat­i­cal for­mu­las in LaTeX, I use Vim. Vim is a pow­er­ful gen­er­al pur­pose text ed­i­tor that’s very ex­ten­si­ble. I use it for writ­ing code, LaTeX, mark­down, … ba­si­cal­ly every­thing that’s text-​based. It has a fair­ly steep learn­ing curve, but once you’ve got the ba­sics down, it’s hard to get back to an ed­i­tor with­out Vim key­bind­ings. Here’s what my screen looks like when I’m edit­ing LaTeX:

Vim and Zathura

On the left you see Vim and on the right my pdf view­er, Za­thu­ra, which also has Vim-​like key­bind­ings. I’m using Ubun­tu with bspwm as my win­dow man­ag­er. The LaTeX plu­g­in I’m using in Vim is vim­tex. It pro­vides syn­tax high­light­ing, table of con­tents view, sync­tex, etc. Using vim-​plug, I con­fig­ured it as fol­lows:

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'

The last two lines con­fig­ure the con­ceal­ment. This is a fea­ture where LaTeX code is re­placed or made in­vis­i­ble when your cur­sor is not on that line. By mak­ing \[, \], $ in­vis­i­ble, they’re less ob­tru­sive which gives you a bet­ter overview of the doc­u­ment. This fea­ture also re­places \bigcap by by , \in by etc. The fol­low­ing an­i­ma­tion should make that clear.


With this set up, I come to the crux of this blog post: writ­ing LaTeX as fast as the lec­tur­er can write on the black­board. This is where snip­pets come into play.


What’s a snippet?

A snip­pet is a short reusable piece of text that can be trig­gered by some other text. For ex­am­ple, when I type sign and press Tab, the word sign will be ex­pand­ed to a sig­na­ture:


Snip­pets can also be dy­nam­ic: when I type today and press Tab, the word today will be re­placed by the cur­rent date, and box Tab be­comes a box that au­to­mat­i­cal­ly grows in size.



You can even use one snip­pet in­side an­oth­er:


Using UltiSnips to create snippets

I use the plu­g­in Ul­tiSnips to man­age my snip­pets. My con­fig­u­ra­tion is

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

The code for the sign snip­pet is the fol­low­ing:

snippet sign "Signature"
Yours sincerely,

Gilles Castel

For dy­nam­ic snip­pets, you can put code be­tween back­ticks `` which will be run when the snip­pet is ex­pand­ed. Here, I’ve used bash to for­mat the cur­rent date: date + %F.

snippet today "Date"
`date +%F`

You can also use Python in­side a `!p ... ` block. Have a look at the code for the box snip­pet:

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

These Python code blocks will be re­placed by the value of the vari­able snip.rv. In­side these blocks, you have ac­cess to the cur­rent state of the snip­pet, e.g. t[1] con­tains the first tab stop, fn the cur­rent file­name, …

LaTeX snippets

Using snip­pets, writ­ing LaTeX is a lot faster than writ­ing it by hand. Es­pe­cial­ly some of the more com­plex snip­pets can save you a lot of time and frus­tra­tion. Let’s begin with some sim­ple snip­pets.


To in­sert an en­vi­ron­ment, all I have to do is type beg at the be­gin­ning of a line. Then I type the name of the en­vi­ron­ment, which is mir­rored in the \end{} com­mand. Press­ing Tab places the cur­sor in­side the newly cre­at­ed en­vi­ron­ment.


The code for this snip­pet is the fol­low­ing.

snippet beg "begin{} / end{}" bA

The b means that this snip­pet will only be ex­pand­ed at the be­gin­ning of a line and A stands for auto ex­pand, which means I do not have to press Tab to ex­pand the snip­pet. Tab stops — i.e. places you can jump to by press­ing Tab and Shift+Tab — are rep­re­sent­ed by $1, $2, … and the last one with $0.

Inline and display math

Two of my most fre­quent­ly used snip­pets are mk and dm. They’re the snip­pets re­spon­si­ble for start­ing math mode. The first one is a snip­pet for in­line math, the sec­ond one for dis­played math.


The snip­pet for in­line math is ‘smart’: it knows when to in­sert a space after the dol­lar sign. When I start typ­ing a word di­rect­ly be­hind the clos­ing $, it adds a space. How­ev­er, when I type a non-​word char­ac­ter, it does not add a space, which would be pre­ferred for ex­am­ple in the case of $p$-value.


The code for this snip­pet is the fol­low­ing.

snippet mk "Math" wA
if t[2] and t[2][0] not in [',', '.', '?', '-', ' ']:
    snip.rv = ' '
    snip.rv = ''

The w at the end of the first line means that this snip­pet will ex­pand at word bound­aries, so e.g. hellomk won’t ex­pand, but hello mk will.

The snip­pet for dis­played math is more sim­ple, but it also is quite handy; it makes me never for­get end­ing equa­tions with a pe­ri­od.


snippet dm "Math" wA
.\] $0

Sub- and superscripts

An­oth­er use­ful snip­pet is one for sub­scripts. It changes changes a1 to a_1 and a_12 to a_{12}.


The code for this snip­pet uses a reg­u­lar ex­pres­sion for its trig­ger. It ex­pands the snip­pet when you type a char­ac­ter fol­lowed by a digit, which en­cod­ed by [A-Za-z]\d, or a char­ac­ter fol­lowed by _ and two dig­its: [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)`

snippet '([A-Za-z])_(\d\d)' "auto subscript2" wrA
`!p snip.rv = match.group(1)`_{`!p snip.rv = match.group(2)`}

When you wrap parts of a reg­u­lar ex­pres­sion in a group using paren­the­sis, e.g. (\d\d), you can use them in the ex­pan­sion of the snip­pet via match.group(i) in Python.

As for su­per­scripts, I use td, which be­comes ^{}. How­ev­er, for squared, cubed, com­ple­ment and a hand­ful of other com­mon ones, I use ded­i­cat­ed snip­pets such as sr, cb and comp.


snippet sr "^2" iA

snippet cb "^3" iA

snippet compl "complement" iA

snippet td "superscript" iA


One of my most con­ve­nient snip­pets is one for frac­tions. This makes the fol­low­ing ex­pan­sions:

// \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)}{}


The code for the first one is easy:

snippet // "Fraction" iA

The sec­ond and third ex­am­ples are made pos­si­ble using reg­u­lar ex­pres­sions to match for ex­pres­sions like 3/, 4ac/, 6\pi^2/, a_2/, etc.

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

As you can see, reg­u­lar ex­pres­sions can be­come quite over­whelm­ing, but here’s a di­a­gram that should ex­plain it:

Diagram of the regular expression

In the fourth and fifth cases, it tries to find the match­ing paren­the­sis. As this isn’t pos­si­ble using the reg­u­lar ex­pres­sion en­gine of Ul­tiSnips, I re­sort­ed to using Python:

priority 1000
snippet '^.*\)/' "() Fraction" wrA
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] + "}"

The last snip­pet con­cern­ing frac­tions I’d like to share is one that uses your se­lec­tion to make a frac­tion. You can use it by first se­lect­ing some text, then press­ing Tab, typ­ing / and press­ing Tab again.


The code makes use of the ${VISUAL} vari­able that rep­re­sents your se­lec­tion.

snippet / "Fraction" iA

Sympy and Mathematica

An­oth­er cool — but less used — snip­pet is one that uses sympy to eval­u­ate math­e­mat­i­cal ex­pres­sions. For ex­am­ple: sympy Tab ex­pands to sympy | sympy, and sympy 1 + 1 sympy Tab ex­pands to 2.


snippet sympy "sympy block " w
sympy $1 sympy$0

priority 10000
snippet 'sympy(.*)sympy' "evaluate sympy" wr
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)
snip.rv = eval('latex(' + match.group(1).replace('\\', '') \
    .replace('^', '**') \
    .replace('{', '(') \
    .replace('}', ')') + ')')

For the Math­e­mat­i­ca users out there, you can do some­thing sim­i­lar:


priority 1000
snippet math "mathematica block" w
math $1 math$0

priority 10000
snippet 'math(.*)math' "evaluate mathematica" wr
import subprocess
code = 'ToString[' + match.group(1) + ', TeXForm]'
snip.rv = subprocess.check_output(['wolframscript', '-code', code])

Postfix snippets

Some other snip­pets I find worth shar­ing are post­fix snip­pets. Ex­am­ples of such snip­pets are phat\hat{p} and zbar\overline{z}. A sim­i­lar snip­pet is a post­fix vec­tor, for ex­am­ple v,.\vec{v} and v.,\vec{v}. The order of , and . doesn’t mat­ter, so I can press them both at the same time. These snip­pets are a real time-​saver, be­cause you can type in the same order the lec­tur­er writes on the black­board.


Note that I can still use bar and hat pre­fix too, as I’ve added them with a lower pri­or­i­ty. The code for those snip­pets is:

priority 10
snippet "bar" "bar" riA

priority 100
snippet "([a-zA-Z])bar" "bar" riA
\overline{`!p snip.rv=match.group(1)`}
priority 10
snippet "hat" "hat" riA

priority 100
snippet "([a-zA-Z])hat" "hat" riA
\hat{`!p snip.rv=match.group(1)`}
snippet "(\\?\w+)(,\.|\.,)" "Vector postfix" riA
\vec{`!p snip.rv=match.group(1)`}

Other snippets

I have about 100 other com­mon­ly used snip­pets. They are avail­able here. Most of them are quite sim­ple. For ex­am­ple, !> be­comes \mapsto, -> be­comes \to, etc.


fun be­comes f: \R \to \R :, !>\mapsto, ->\to, cc\subset.


lim be­comes \lim_{n \to \infty}, sum\sum_{n = 1}^{\infty}, ooo\infty



Course specific snippets

Be­side my com­mon­ly used snip­pets, I also have course spe­cif­ic snip­pets. These are loaded by adding the fol­low­ing to my .vimrc:

set rtp+=~/current_course

where current_course is a sym­link to my cur­rent­ly ac­ti­vat­ed course (more about that in an­oth­er blog post). In that fold­er, I have a file ~/current_course/UltiSnips/tex.snippets in which I in­clude course spe­cif­ic snip­pets. For ex­am­ple, for quan­tum me­chan­ics, I have snip­pets for bra/ket no­ta­tion.

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

As \psi is used a lot in quan­tum me­chan­ics, I re­place all in­stances of q in a braket with \psi when ex­pand­ed.


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

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

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')`}


One thing to con­sid­er when writ­ing these snip­pets is, ‘will these snip­pets col­lide with usual text?’ For ex­am­ple, ac­cord­ing to my dic­tio­nary, there are about 72 words in Eng­lish and 2000 words in Dutch that con­tain sr, which means that while I’m typ­ing the word disregard, the sr would ex­pand to ^2, giv­ing me di^2egard.

The so­lu­tion to this prob­lem is adding a con­text to snip­pets. Using the syn­tax high­light­ing of Vim, it can be de­ter­mined whether or not Ul­tiSnips should ex­pand the snip­pet de­pend­ing if you’re in math or text. Add the fol­low­ing to the top of your snip­pets file:

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

def comment(): 
    return vim.eval('vimtex#syntax#in_comment()') == '1'

def env(name):
    [x,y] = vim.eval("vimtex#env#is_inside('" + name + "')") 
    return x != '0' and x != '0'


Now you can add context "math()" to the snip­pets you’d only want to ex­pand in a math­e­mat­i­cal con­text.

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

Note that a ‘math­e­mat­i­cal con­text’ is a sub­tle thing. Some­times you add some text in­side a math en­vi­ron­ment by using \text{...}. In that case, you do not want snip­pets to ex­pand. How­ev­er, in the fol­low­ing case: \[ \text{$...$} \], they should ex­pand. The fol­low­ing an­i­ma­tion il­lus­trates these sub­tleties.


Sim­i­lar­ly, you can add context "env('itemize')" to snip­pets that only should ex­pand in an itemize en­vi­ron­ment or context "comment()" for snip­pets in com­ments.

This section was edited on 2020-12-24. The new version of vimtex makes the code for detecting the context a lot simpler. Click to see old version
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', 'AmsA', 'AmsB', 'AmsC',
'AmsD', 'AmsE', 'AmsF', 'AmsG', 'AmsAS', 'AmsBS', 'AmsCS', 'AmsDS', 'AmsES',
'AmsFS', 'AmsGS' ]]

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))")
		first = next(
            i for i in reversed(synstackids)
            if i in texIgnoreMathZoneIds or i in texMathZoneIds
		return first != ignore
	except StopIteration:
		return False

Correcting spelling mistakes on the fly

While in­sert­ing math­e­mat­ics is an im­por­tant part of my note-​taking setup, most of the time I’m typ­ing Eng­lish. At about 80 words per minute, my typ­ing skills are not bad, but I still make a lot of typos. This is why I added a key­bind­ing to Vim that cor­rects the spelling mis­takes, with­out in­ter­rupt­ing my flow. When I press Ctrl+L while I’m typ­ing, the pre­vi­ous spelling mis­take is cor­rect­ed. It looks like this:


My con­fig­u­ra­tion for spell check is the fol­low­ing:

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

It ba­si­cal­ly jumps to the pre­vi­ous spelling mis­take [s, then picks the first sug­ges­tion 1z=, and then jumps back `]a. The <c-g>u in the mid­dle make it pos­si­ble to undo the spelling cor­rec­tion quick­ly.

In conclusion

Using snip­pets in Vim, writ­ing LaTeX is no longer an an­noy­ance, but rather a plea­sure. In com­bi­na­tion with spell check on the fly, it al­lows for a com­fort­able math­e­mat­i­cal note-​taking setup. A few pieces are miss­ing though, for ex­am­ple draw­ing fig­ures dig­i­tal­ly and em­bed­ding them in a LaTeX doc­u­ment. This is a topic I’d like to tack­le in a fu­ture blog post.

Liked this blog post? Con­sid­er buy­ing me a cof­fee!

Written by Gilles Castel, who lives in Belgium studying mathematics at the university of Leuven.