2. An UltiSnips guide for LaTeX workflows

Last modified: 8 June 2022

This is part two in a seven-part series explaining how to use the Vim or Neovim text editors to efficiently write LaTeX documents. This article covers snippets, which are templates of commonly reused code that, when used properly, will dramatically speed up your LaTeX writing.

Contents of this article

What snippets do

Snippets are templates of commonly used code (for example the boilerplate code for typical LaTeX environments and commands) inserted into text dynamically using short (e.g. two- or three-character) triggers. Without wishing to overstate the case, good use of snippets is the single most important step in the process of writing LaTeX efficiently and painlessly. Here is a simple example:

Writing LaTeX quickly with auto-trigger snippets

Getting started with UltiSnips

This tutorial will use the UltiSnips plugin, which is the most mature out of the menagerie of Vim snippet plugins. If you use Neovim, note that UltiSnips’s support of Neovim is “best-effort only”. Don’t let this discourage you—I and many other Neovim users daily drive Ultisnips and Neovim without any issues, and things will probably be fine for you, too. If you use regular Vim, you should be fine in any case.

Installation

Install UltiSnips like any other Vim plugin using your plugin installation method of choice. Because the UltiSnips plugin uses Python…

UltiSnips is a snippet engine only and intentionally ships without snippets—you have to write your own or use an existing snippet database. The canonical source of existing snippets is GitHub user honza’s vim-snippets repository. Whether you download someone else’s snippets, write your own, or use a mixture of both, you should know:

  1. where the text files holding your snippets are stored on your local file system, and
  2. how to write, edit, and otherwise tweak snippets to suit your particular needs, so you are not stuck using someone else’s without the possibility of customization.

Both questions are answered in this article.

First steps: snippet trigger and tabstop navigation keys

After installing UltiSnips you should configure…

  1. the key you use to trigger (expand) snippets, which is set using the global variable g:UltiSnipsExpandTrigger,
  2. the key you use to move forward through a snippet’s tabstops, which is set using g:UltiSnipsJumpForwardTrigger, and
  3. the key you use to move backward through a snippet’s tabstops, which is set with g:UltiSnipsJumpBackwardTrigger.

For orientation, here is an example configuration, which you would place the code in your vimrc or init.vim:

" This code should go in your vimrc or init.vim
let g:UltiSnipsExpandTrigger       = '<Tab>'    " use Tab to expand snippets
let g:UltiSnipsJumpForwardTrigger  = '<Tab>'    " use Tab to move forward through tabstops
let g:UltiSnipsJumpBackwardTrigger = '<S-Tab>'  " use Shift-Tab to move backward through tabstops

Explanation: this code would make the <Tab> key trigger snippets and navigate forward through snippet tabstops (yes, UltiSnips lets you use the same key for both expansion and tabstop navigation), and make the key combination <Shift>+<Tab> navigate backward through tabstops.

In the above GIF, I am actually using jk as the g:UltiSnipsJumpForwardTrigger key. I find this home-row combination more efficient than <Tab>, but it takes some getting used to; scroll down to the (Subjective) practical tips for fast editing at the bottom of this article for more on using jk as a jump-forward key and similar tips.

See :help UltiSnips-trigger-key-mappings for official documentation of trigger keys. For fine-grained control one can also work directly with functions controlling expand and jump behavior; for more information on this see :help UltiSnips-trigger-functions. For most users just setting the three global trigger key variables, as in the example above, should suffice.

A home for your snippets

You store snippets in text files with the .snippets extension. The file’s base name determines which Vim filetype the snippets apply to. For example, snippets inside the file tex.snippets would apply to files with filetype=tex. If you want certain snippets to apply globally to all file types, place these global snippets in the file all.snippets, which is documented towards the bottom of :help UltiSnips-how-snippets-are-loaded.

By default, UltiSnips expects your .snippet files to live in directories called UltiSnips, which, if you wanted, you could place anywhere in your Vim runtimepath. You can use folder names other than the default UltiSnips, too—the snippet directory name is controlled with the global variable g:UltiSnipsSnippetDirectories. From :help UltiSnips-how-snippets-are-loaded,

UltiSnips will search each runtimepath directory for the subdirectory names defined in g:UltiSnipsSnippetDirectories in the order they are defined.

For example, to use MySnippets as a snippet directory, you would place the following Vimscript in your vimrc or init.vim:

" Use both `UltiSnips` and `MySnippets` as snippet directories
 let g:UltiSnipsSnippetDirectories=["UltiSnips", "MySnippets"]

UltiSnips would then load *.snippet files from all UltiSnips and MySnippets directories in your Vim runtimepath.

Possible optimization: if, like me, you use only a single predefined snippet directory and don’t need UltiSnips to scan your entire runtimepath each time you open Vim (which can slow down Vim’s start-up time), set g:UltiSnipsSnippetDirectories to use a single, absolute path to your snippets directory, for example

let g:UltiSnipsSnippetDirectories=[$HOME.'/.vim/UltiSnips']          " using Vim
let g:UltiSnipsSnippetDirectories=[$HOME.'/.config/nvim/UltiSnips']  " using Neovim

This behavior is documented in :help UltiSnips-how-snippets-are-loaded. (The . joining $HOME and '/.vim/UltiSnips' is the Vimscript string concatenation operator.)

Snippet folders

You might prefer to further organize filetype-specific snippets into multiple files of their own. To do so, make a folder named with the target filetype inside your snippets directory. UltiSnips will then load all .snippet files inside this folder, regardless of their basename. Again, this behavior is documented in :help UltiSnips-how-snippets-are-loaded. As a concrete example, a selection of my UltiSnips directory looks like this:

${HOME}/.vim/UltiSnips/           # Vim
${HOME}/.config/nvim/UltiSnips/   # Neovim
├── all.snippets
├── markdown.snippets
├── python.snippets
└── tex
    ├── delimiters.snippets
    ├── environments.snippets
    ├── fonts.snippets
    └── math.snippets

Explanation: I have a lot of tex snippets, so I prefer to further organize them in a dedicated directory, while a single file suffices for all, markdown, and python.

Watch the screencasts!

Quite a few years ago now, Holger Rapp, the author of UltiSnips, created four screencasts demonstrating the plugin’s features:

They’re old but gold, and pack an impressively thorough demonstration of UltiSnips’s capabilities into about 20 minutes of video. I strongly suggest your watch them—you will find many of the features described in this article covered from a different perspective in the screencasts.

Writing Snippets

TLDR: create a {filetype}.snippets file in your UltiSnips directory (e.g. tex.snippets) and write your snippets inside this file using the syntax described in :help UltiSnips-basic-syntax.

Anatomy of an UltiSnips snippet

The general form of an UltiSnips snippet is:

snippet {trigger} ["description" [options]]
{snippet body}
endsnippet

The trigger and snippet body are mandatory, while "description" (which should be enclosed in quotes) and options are optional; options can be included only if a "description" is also provided. The keywords snippet and endsnippet define the beginning and end of the snippet. See :help UltiSnips-authoring-snippets for the relevant documentation.

An apology about syntax highlighting

Please excuse the sub-optimal syntax highlighting of UltiSnips snippet code blocks throughout this article. This website is written with Jekyll and GitHub Pages, which use the rogue Ruby Gem for syntax highlighting. At the time of writing, rogue does not support the UltiSnips snippet language (see here for the current list of rogue lexers), and so the snippet code looks meh. For lack of a better option, I shuffle between plain text and generic shell-script highlighting (which at least highlights comments), neither of which are particularly satisfactory. I might or might not get around to fixing this by just writing and contributing an UltiSnips lexer for rouge; for the time being, we’ll have to put up less-than-perfect snippet highlighting.

Options

You’ll need to use a few options to get the full UltiSnips experience. All options are clearly documented at :help UltiSnips-snippet-options, and I’ll summarize here only what is necessary for understanding the snippets that appear later in this document. Based on my (subjective) experience, with a focus on LaTeX files, here are some good options to know:

Assorted snippet syntax rules

Tabstops

Tabstops are predefined positions within a snippet body to which you can move by pressing the key mapped to g:UltiSnipsJumpForwardTrigger. Tabstops allow you to efficiently navigate through a snippet’s variable content while skipping the positions of static content. You navigate through tabstops by pressing, in insert mode, the keys mapped to g:UltiSnipsJumpForwardTrigger and g:UltiSnipsJumpBackwardTrigger. Since that might sound vague, here is an example of jumping through the tabstops for figure path, caption, and label in a LaTeX figure environment:

Showing how snippet tabstops work

Paraphrasing from :help UltiSnips-tabstops:

As far as I’m aware, this is a similar tabstop syntax to that used in the popular IDE Visual Studio Code.

Some example LaTeX snippets

For orientation, here are two examples: one maps tt to the \texttt command and the other maps ff to the \frac command. Note that (at least for me) the snippet expands correctly without escaping the \, {, and } characters as suggested in :help UltiSnips-character-escaping (see the second bullet in Assorted snippet syntax rules).

snippet tt "The \texttt{} command for typewriter-style font"
\texttt{$1}$0
endsnippet

snippet ff "The LaTeX \frac{}{} command"
\frac{$1}{$2}$0
endsnippet

Here are the above \texttt{} and \frac{}{} snippets in action:

The \texttt and \frac snippets in action

Useful: tabstop placeholders

Placeholders are used to enrich a tabstop with a description or default text. The syntax for defining placeholder text is ${1:placeholder}. Placeholders are documented at :help UltiSnips-placeholders. Here is a real-world example I used to remind myself the correct order for the URL and display text in the hyperref package’s href command:

snippet hr "The hyperref package's \href{}{} command (for url links)"
\href{${1:url}}{${2:display name}}$0
endsnippet

Here is what this snippet looks like in practice:

Demonstrating the tabstop placeholder

Useful: mirrored tabstops

Mirrors allow you to reuse a tabstop’s content in multiple locations throughout the snippet body. In practice, you might use mirrored tabstops for the \begin and \end fields of a LaTeX environment. Here is an example:

Demonstrating mirrored tabstops

The syntax for mirrored tabstops is what you might intuitively expect: just repeat the tabstop you wish to mirror. For example, following is the code for the snippet shown in the above GIF; note how the $1 tabstop containing the environment name is mirrored in both the \begin and \end commands:

snippet env "New LaTeX environment" b
\begin{$1}
    $2
\end{$1}
$0
endsnippet

The b options ensures the snippet only expands at the start of line; see the Options section for review of common UltiSnips options. Mirrored tabstops are documented at :help UltiSnips-mirrors.

Useful: the visual placeholder

The visual placeholder enables you to use text selected in Vim’s visual mode as the content of a snippet body. The visual placeholder is useful when you want to surround existing text with a snippet (e.g. to place a sentence inside a LaTeX italics command or to surround a word with quotation marks). You can have one visual placeholder per snippet, and you specify it with the ${VISUAL} keyword. This usually is (but does not have to be) integrated into tabstops.

As an example, here is a snippet to the LaTeX \textit command, using a visual placeholder to make it easer to surround text in italics:

snippet tii "The \textit{} command for italic font"
\textit{${1:${VISUAL:}}}$0
endsnippet

And here is what this snippet looks like in action:

Demonstrating the visual placeholder

The visual placeholder is documented at :help UltiSnips-visual-placeholder and explained on video in the UltiSnips screencast Episode 3: What’s new in version 2.0; I encourage you to watch the video for orientation, if needed.

Dynamically-evaluated code inside snippets

It is possible to add dynamically-evaluated code to snippet bodies (UltiSnips calls this “code interpolation”). Shell script, Vimscript, and Python are all supported. Interpolation is covered in :help UltiSnips-interpolation and in the UltiSnips screencast Episode 4: Python Interpolation. I will only cover two examples I subjectively find to be most useful for LaTeX:

  1. making certain snippets expand only when the trigger is typed in LaTeX math environments, which is called custom context expansion, and

  2. accessing characters captured by a regular expression trigger’s capture group.

Custom context expansion and VimTeX’s syntax detection

UltiSnips’s custom context features (see :help UltiSnips-custom-context-snippets) give you essentially arbitrary control over when snippets expand, and one very useful LaTeX application is expanding a snippet only if its trigger is typed in a LaTeX math context. As an example of why this might be useful:

You will need GitHub user lervag’s VimTeX plugin for math-context expansion. (I cover VimTeX in much more detail in the fourth article in this series.) The VimTeX plugin, among many other things, provides the user with the function vimtex#syntax#in_mathzone(), which returns 1 if the cursor is inside a LaTeX math zone (e.g. between $ $ for inline math, inside an equation environment, etc…) and 0 otherwise. This function isn’t explicitly mentioned in the VimTeX documentation, but you can find it in the VimTeX source code at vimtex/autoload/vimtex/syntax.vim.

You can integrate VimTeX’s math zone detection with UltiSnips’s context feature as follows:

# include this code block at the top of a *.snippets file...
# ----------------------------- #
global !p
def math():
  return vim.eval('vimtex#syntax#in_mathzone()') == '1'
endglobal
# ----------------------------- #
# ...then place 'context "math()"' above any snippets you want to expand only in math mode

context "math()"
snippet ff "This \frac{}{} snippet expands only a LaTeX math context"
\frac{$1}{$2}$0
endsnippet

My original source for the implementation of math-context expansion: https://castel.dev/post/lecture-notes-1/#context.

Regex snippet triggers

For our purposes, if you aren’t familiar with them, regular expressions let you (among many other things) implement conditional pattern matching in snippet triggers. You could use a regular expression trigger, for example, to do something like “make ^ expand to a superscript snippet like ^{$1}$0, but only if the ^ trigger immediately follows an alphanumeric character”.

A formal explanation of regular expressions falls beyond the scope of this work, and I offer the examples below in a “cookbook” style in the hope that you can adapt the ideas to your own use cases. Regex tutorials abound on the internet; if you need a place to start, I recommend Corey Schafer’s tutorial on YouTube.

  1. This class of triggers suppresses expansion following alphanumeric text and permits expansion after blank space, punctuation marks, braces and other delimiters, etc…

    snippet "([^a-zA-Z])trigger" "Expands if 'trigger' is typed after characters other than a-z or A-Z" r
    `!p snip.rv = match.group(1)`snippet body
    endsnippet
    
    snippet "(^|[^a-zA-Z])trigger" "Expands on a new line or after characters other than a-z or A-Z" r
    `!p snip.rv = match.group(1)`snippet body
    endsnippet
    
    # This trigger suppresses numbers, too
    snippet "(^|[\W])trigger" "Expands on a new line or after characters other than 0-9, a-z, or A-Z" r
    `!p snip.rv = match.group(1)`snippet body
    endsnippet
    

    This is by far my most-used class of regex triggers. Here are some example use cases:

    • Make mm expand to $ $ (inline math), including on new lines, but not in words like “communication”, “command”, etc…
      snippet "(^|[^a-zA-Z])mm" "Inline LaTeX math" rA
      `!p snip.rv = match.group(1)`\$ ${1:${VISUAL:}} \$$0
      endsnippet
      

      Note that the dollar signs used for the inline math must be escaped (i.e. written \$ instead of just $) to avoid conflict with UltiSnips tabstops, as described in :help UltiSnips-character-escaping.

    • Make ee expand to e^{} (Euler’s number raised to a power) after spaces, (, {, and other delimiters, but not in words like “see”, “feel”, etc…
      snippet "([^a-zA-Z])ee" "e^{} supercript" rA
      `!p snip.rv = match.group(1)`e^{${1:${VISUAL:}}}$0
      endsnippet
      
    • Make ff expand to frac{}{} but not in words like “off”, “offer”, etc…
      snippet "(^|[^a-zA-Z])ff" "\frac{}{}" rA
      `!p snip.rv = match.group(1)`\frac{${1:${VISUAL:}}}{$2}$0
      endsnippet
      

      The line `!p snip.rv = match.group(1)` inserts the regex group captured by the trigger parentheses back into the original text. Since that might sound vague, try omitting `!p snip.rv = match.group(1)` from any of the above snippets and seeing what happens—the first character in the snippet trigger disappears after the snippet expands.

  2. This class of triggers expands only after alphanumerical characters (\w) or the characters }, ), ], and |.

    snippet "([\w])trigger" "Expands if 'trigger' is typed after 0-9, a-z, and  A-Z" r
    `!p snip.rv = match.group(1)`snippet body
    endsnippet
    
    # Of course, modify the }, ), ], and | characters as you wish
    snippet "([\w]|[\}\)\]\|])trigger" "Expands after 0-9, a-z, A-Z and }, ), ], and |" r
    `!p snip.rv = match.group(1)`snippet body
    endsnippet
    
    # This trigger suppresses expansion after numbers
    snippet "([a-zA-Z]|[\}\)\]\|])trigger" "Expands after a-z, A-Z and }, ), ], and |" r
    `!p snip.rv = match.group(1)`snippet body
    endsnippet
    

    I don’t use this one often, but here is one example I really like. It makes 00 expand to the _{0} subscript after letters and closing delimiters, but not in numbers like 100:

    snippet "([a-zA-Z]|[\}\)\]\|'])00" "Automatic 0 subscript" rA
    `!p snip.rv = match.group(1)`_{0}
    endsnippet
    

    Here is the above snippet in action:

    The 0 subscript snippet in action

Combined with math-context expansion, these two classes of regex triggers cover the majority of my use cases and should give you enough to get started writing your own. Note that you can do much fancier stuff than this. See the UltiSnips documentation or look through the snippets in vim-snippets for inspiration.

Tip: Refreshing snippets

The function UltiSnips#RefreshSnippets refreshes the snippets in the current Vim instance to reflect the contents of your snippets directory. Here’s an example use case:

This workflow comes up regularly if you use snippets often, and I suggest writing a key mapping in your vimrc to call the UltiSnips#RefreshSnippets() function, for example

" Use <leader>u in normal mode to refresh UltiSnips snippets
nnoremap <leader>u <Cmd>call UltiSnips#RefreshSnippets()<CR>

In case it looks unfamiliar, the above code snippet is a Vim key mapping, a standard Vim configuration tool described in much more detail in the series’s final article, 7. A Vimscript Primer for Filetype-Specific Workflows.

(Subjective) practical tips for fast editing

I’m writing this with math-heavy LaTeX in real-time university lectures in mind, where speed is crucial; these tips might be overkill for more relaxed use cases. In no particular order, here are some useful tips based on my personal experience:

Tip: A snippet for writing snippets

The following snippet makes it easier to write more snippets. To use it, create the file ~/.vim/UltiSnips/snippets.snippets, and inside it paste the following code:

snippet snip "A snippet for writing Ultisnips snippets" b
`!p snip.rv = "snippet"` ${1:trigger} "${2:Description}" ${3:options}
$4
`!p snip.rv = "endsnippet"`
$0
endsnippet

This will insert a snippet template when you type snip, followed by the snippet trigger key stored in g:UltiSnipsExpandTrigger, at the beginning of a line in a *.snippets file in insert mode. Here’s what this looks like in practice:

The snippet-writing snippet in action

The use of `!p snip.rv = "snippet"` needs some explanation—this uses the UltiSnips Python interpolation feature, described in the section on dynamically-evaluated code inside snippets—to insert the literal string snippet in place of `!p snip.rv = "snippet"`. The naive implementation would be to write

# THIS SNIPPET WON'T WORK---IT'S JUST FOR EXPLANATION!
snippet snip "A snippet for writing Ultisnips snippets" b
snippet ${1:trigger} "${2:Description}" ${3:options}
$4
endsnippet
$0
endsnippet

but this would make the UltiSnips parser think that the line snippet ${1:trigger}... starts a new snippet definition, when the goal is to insert the literal string snippet ${1:trigger}... into another file. In any case, this problem is specific to using the string snippet inside a snippet, and most snippets are much easier to write than this.

The original writing, images, and animations in this series are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License.
CC BY-NC 4.0