2. A LuaSnip guide for LaTeX workflows

Last modified: 31 October 2022

You might be interested in this article for two reasons:

  1. As a standalone guide to writing snippets with the LuaSnip plugin (this article is LaTeX-heavy, but it is applicable to any language).
  2. As part two in a seven-part series explaining how to use the Vim or Neovim text editors to efficiently write LaTeX documents.

There is also an UltiSnips version of this article.

Warning: the article is long

This article is long, which kind of sucks, I know. To help you out, here’s how the article is organized:

  1. A short intro to snippets for people reading along with the LaTeX series
  2. A TLDR example of hello world with LuaSnip
  3. Boring house-keeping stuff (installation, configuration, file organization, snippet syntax etc.)
  4. Actually writing snippets—you probably came for this section.
  5. Some practical tips from the perspective of a real-life user.

Feel free to skim or skip the boring stuff and jump right to actually writing snippets.

Contents of this article

What snippets do

I know what snippets do, next section please.

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), easy-to-type character sequences called triggers. Without wishing to overstate the case, good use of snippets is the single most important step in the process of writing LaTeX—and any other verbose markup or programming language—efficiently and painlessly.

Here is a video demonstrating full-speed, real-life examples. And here is a simple example using snippets to create and navigate through a LaTeX figure environment, quickly typeset an equation, and easily insert commands for Greek letters.

Writing LaTeX quickly with autotrigger snippets

TLDR hello world example

I’m beginning with a hello world snippet instead of a bunch of theory. For now feel free to just copy and paste along with the article, and we’ll explain what’s going on in much more detail later. (Or if you prefer to begin with fundamentals, you can skip this hello world example and jump to getting started with LuaSnip.)

In a very TLDR style, here’s a LuaSnip-flavored hello world:

  1. Install LuaSnip (I’m assuming you’re comfortable installing Vim plugins.)

  2. In your init.vim/init.lua, set key bindings to trigger and navigate through snippets:

    I use an init.lua

    Place this in your init.lua:

    -- Yes, we're just executing a bunch of Vimscript using vim.cmd
    vim.cmd[[
    " Use Tab to expand and jump through snippets
    imap <silent><expr> <Tab> luasnip#expand_or_jumpable() ? '<Plug>luasnip-expand-or-jump' : '<Tab>' 
    smap <silent><expr> <Tab> luasnip#jumpable(1) ? '<Plug>luasnip-jump-next' : '<Tab>'
    
    " Use Shift-Tab to jump backwards through snippets
    imap <silent><expr> <S-Tab> luasnip#jumpable(-1) ? '<Plug>luasnip-jump-prev' : '<S-Tab>'
    smap <silent><expr> <S-Tab> luasnip#jumpable(-1) ? '<Plug>luasnip-jump-prev' : '<S-Tab>'
    ]]
    
    I use an init.vim

    Place this in your init.vim:

    " Use Tab to expand and jump through snippets
    imap <silent><expr> <Tab> luasnip#expand_or_jumpable() ? '<Plug>luasnip-expand-or-jump' : '<Tab>' 
    smap <silent><expr> <Tab> luasnip#jumpable(1) ? '<Plug>luasnip-jump-next' : '<Tab>'
    
    " Use Shift-Tab to jump backwards through snippets
    imap <silent><expr> <S-Tab> luasnip#jumpable(-1) ? '<Plug>luasnip-jump-prev' : '<S-Tab>'
    smap <silent><expr> <S-Tab> luasnip#jumpable(-1) ? '<Plug>luasnip-jump-prev' : '<S-Tab>'
    
  3. Create a snippets directory at ${HOME}/.config/nvim/LuaSnip/, inside it create an empty snippets file called all.lua, and inside it paste:

    -- Place this in ${HOME}/.config/nvim/LuaSnip/all.lua
    return {
      -- A snippet that expands the trigger "hi" into the string "Hello, world!".
      require("luasnip").snippet(
        { trig = "hi" },
        { t("Hello, world!") }
      ),
    
      -- To return multiple snippets, use one `return` statement per snippet file
      -- and return a table of Lua snippets.
      require("luasnip").snippet(
        { trig = "foo" },
        { t("Another snippet.") }
      ),
    }
    
  4. From your init.vim/init.lua, use the following code to load the snippet files in the just-created snippets directory at ${HOME}/.config/nvim/LuaSnip/:

    I use an init.lua
    -- Place this in your init.lua
    require("luasnip.loaders.from_lua").load({paths = "~/.config/nvim/LuaSnip/"})
    
    I use an init.vim
    " Place this in your init.vim
    lua require("luasnip.loaders.from_lua").load({paths = "~/.config/nvim/LuaSnip/"})
    
  5. Open a new Neovim instance (any file name/extension will work here because all.lua is a special snippet file that applies to all filetypes).

  6. Enter insert mode and write hi (because we wrote a snippet with trigger trig="hi"). With your cursor at the end of hi, press the Tab key (because the Tab key was mapped to snippet expansion a few steps up). The word hi should expand into Hello, world!.

I glossed over a mountain of details here for the sake of a TLDR example. For a more thorough introduction to LuaSnip, buckle up and read on.

Going forward, I’ll perform configuration mostly in Lua. If you use Vimscript, I assume you know how to call Lua from within Vimscript using :help :lua and :help :lua-heredoc.

Getting started with LuaSnip

This tutorial will use LuaSnip, the de-facto snippet plugin in Neovim’s Lua ecosystem.

Should I use UltiSnips or LuaSnip?

There is also an UltiSnips version of this article if you prefer.

Installation

Install LuaSnip like any other Neovim plugin using your plugin installation method of choice (e.g. Packer, Vim-Plug, the native package management system, etc.). I assume you know how to install a plugin—see the LuaSnip README’s installation section for details if needed. LuaSnip has no external dependencies and should be ready to go immediately after installation.

LuaSnip is a snippet engine only and intentionally ships without snippets—you have to write your own or use an existing snippet database. It is possible to use existing snippet repositories (e.g. rafamadriz/friendly-snippets) with some additional configuration—see the LuaSnip README’s add snippets section and :help luasnip-loaders if interested. I encourage you to write your own snippets, but 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.

This article answers both questions.

Two config settings for later

There are two LuaSnip configuration changes we’ll need for later in this guide—one enables autotriggered snippets and the other enables visual selection. You can make these changes by placing the following code somewhere in your Neovim startup configuration, e.g. in your init.lua.

-- Somewhere in your Neovim startup, e.g. init.lua
require("luasnip").config.set_config({ -- Setting LuaSnip config

  -- Enable autotriggered snippets
  enable_autosnippets = true,

  -- Use Tab (or some other key if you prefer) to trigger visual selection
  store_selection_keys = "<Tab>",
})

See the LuaSnip README’s config section for full documentation of configuration options.

Set snippet trigger and tabstop navigation keys

After installing LuaSnip you should configure:

  1. the key you use to trigger (expand) snippets,
  2. the key you use to jump forward through a snippet’s tabstops, and
  3. the key you use to jump backward through a snippet’s tabstops.

Setting these keymaps is easiest to do in Vimscript (because they use Vimscript’s conditional ternary operator), so the examples below are in Vimscript.

Choose one of the following two options:

  1. Option one: use a single key (e.g. Tab) to both expand snippets and to jump forward through snippet tabstops. In this case add something like this to your Neovim config:

    " Expand or jump in insert mode
    imap <silent><expr> <Tab> luasnip#expand_or_jumpable() ? '<Plug>luasnip-expand-or-jump' : '<Tab>' 
     
    " Jump forward through tabstops in visual mode
    smap <silent><expr> <Tab> luasnip#jumpable(1) ? '<Plug>luasnip-jump-next' : '<Tab>'
    

    This code would make the Tab key trigger snippets and navigate forward through snippet tabstops—the decision whether to expand or jump is made by LuaSnip’s expand_or_jumpable function.

  2. Option two: use two different keys (e.g. Tab and Control-f) to expand snippets and jump forward through snippet tabstops. In this case the code looks like this:

    " Expand snippets in insert mode with Tab
    imap <silent><expr> <Tab> luasnip#expandable() ? '<Plug>luasnip-expand-snippet' : '<Tab>'
    
    " Jump forward in through tabstops in insert and visual mode with Control-f
    imap <silent><expr> <C-f> luasnip#jumpable(1) ? '<Plug>luasnip-jump-next' : '<C-f>'
    smap <silent><expr> <C-f> luasnip#jumpable(1) ? '<Plug>luasnip-jump-next' : '<C-f>'
    

Then, set a backward-jump keymap:

" Jump backward through snippet tabstops with Shift-Tab (for example)
imap <silent><expr> <S-Tab> luasnip#jumpable(-1) ? '<Plug>luasnip-jump-prev' : '<S-Tab>'
smap <silent><expr> <S-Tab> luasnip#jumpable(-1) ? '<Plug>luasnip-jump-prev' : '<S-Tab>'

A few notes: (or skip to the next section)

  1. Place the keymap code somewhere in your Neovim startup configuration (e.g. init.lua, init.vim, etc.). If you have a Lua-based config and need help running Vimscript from within Lua files, just enclose the Vimscript within a multiline string and pass it to vim.cmd, e.g.

    -- Any Lua config file, e.g. init.lua
    vim.cmd[[
       " Vimscript goes here!
    ]]
    

    If needed, see :help vim.cmd() for details.

  2. In case it’s unfamiliar, the conditional ternary operator condition ? expr1 : expr2 executes expr1 if condition is true and executes expr2 if condition is false—it is common in C and many other languages. In the first imap mapping, for example, the ternary operator is used to map <Tab> to <Plug>luasnip-expand-or-jump if luasnip#expand_or_jumpable() returns true and to <Tab> if luasnip#expand_or_jumpable() returns false.

  3. You need to apply tabstop navigation in both insert and visual modes, hence the use of both imap and smap for the forward and backward jump mappings. (Well, technically select mode and not visual mode, hence the use of smap and not vmap, but for a typical end user’s purposes select and visual mode look identical. See :help select-mode for details.)

    Also, imap and smap are intentionally used instead of inoremap and snoremap—this is standard (and necessary) practice when defining <Plug> mappings. The <Plug> keyword might look unfamiliar—you can ignore it for now, or, if you’re curious, take a peek at the series’s final article, which explains some basic Vimscript usage, including <Plug> mappings.

  4. Power users: you can implement custom snippet expansion and navigation behavior by working directly with LuaSnip API functions controlling expand and jump behavior—see :help luasnip-api-reference (scroll down to the jumpable(direction) entry) for details. For most users the example mappings given above should be fine.

Finally, you may want to set mappings to cycle through choice nodes:

" Cycle forward through choice nodes with Control-f (for example)
imap <silent><expr> <C-f> luasnip#choice_active() ? '<Plug>luasnip-next-choice' : '<C-f>'
smap <silent><expr> <C-f> luasnip#choice_active() ? '<Plug>luasnip-next-choice' : '<C-f>'

Choice nodes are a more advanced tool that I won’t cover in this article, so you can safely skip this step for now. You can read about choice nodes at :help luasnip-choicenode, but you should probably learn about the basic text and insert nodes in this article first.

Snippet files, directories, and loaders

This section explains where to store snippets on your file system, what file format to use, and how to make LuaSnip load the snippets for actual use.

Warning: LuaSnip offers a lot of choices here, and the required decision-making can be overwhelming for new users. I’ll try my best to guide you through your options and give a sensible recommendation for what to choose.

Snippet format

LuaSnip supports multiple snippet formats. Your first step is to decide which format you will write your snippets in. Your main options are:

  1. Covered in this article: Native LuaSnip snippets written in Lua (support for all LuaSnip features, best integration with the larger Neovim ecosystem).
  2. Use third-party snippets written for another snippet engine (e.g. VS Code, SnipMate) and try to parse them with LuaSnip’s various snippet loaders. Fewer features are available, and complex snippets may not be parsable and will not work.

The rest of this article covers only native LuaSnip snippets written in Lua. I think this makes sense because:

If you want to use third-party snippets the rest of this article will probably not be of much help to you; see :help luasnip-loaders, :help luasnip-vscode and :help luasnip-snipmate instead.

Loading snippets and directory structure

You have two ways to load snippets:

This article covers the Lua loader—I recommend this approach because using dedicated snippet files with the Lua loader decouples your snippets from your Neovim startup configuration. This approach is “cleaner” and more modular than writing snippets directly in, say, your init.lua file. If you want to use the add_snippets function instead, see the documentation in :help luasnip-api-reference—most of this article will still be useful to you because the syntax for writing snippets is the same whether you load snippets with add_snippets or LuaSnip’s loader.

Here’s an overview of how to load snippets from Lua files:

Full syntax for the load call is documented in :help luasnip-loaders.

Snippet filetype subdirectories

You might prefer to further organize filetype-specific snippets into multiple files of their own. To do so, make a subdirectory named with the target filetype inside your main snippets directory. LuaSnip will then load all *.lua files inside this filetype subdirectory, regardless of the individual files’ basenames. As a concrete example, a selection of my LuaSnip directory looks like this:

# Example LuaSnip directory structure
${HOME}/.config/nvim/LuaSnip/
├── all.lua
├── markdown.lua
├── python.lua
└── tex
    ├── delimiters.lua
    ├── environments.lua
    ├── fonts.lua
    └── math.lua

Explanation: I have a lot of tex snippets, so I prefer to further organize them in a dedicated subdirectory with individual files for LaTeX delimiters, environments, and so on, while a single file suffices for all, markdown, and python.

Heads up—some abbreviations

TLDR: LuaSnip defines a globally-available set of abbreviations for common modules that make writing snippets much easier. These abbreviations are listed below, and you’ll see them in this document, the LuaSnip docs, and elsewhere on the Internet. You can now jump to the next section. End TLDR.

LuaSnip provides a set of convenient abbreviations for more compact snippet syntax. For example, you technically define a LuaSnip by calling require("luasnip").snippet(). Since this is a bit verbose, LuaSnip introduces the abbreviations…

-- Two common LuaSnip abbreviations
local ls = require("luasnip")
local s = ls.snippet

…in terms of which you you could then write require("luasnip").snippet() as

-- Three progressively shorter ways to do the same thing---define a snippet
require("luasnip").snippet()
ls.snippet()
s()

Here is a list of the LuaSnip abbreviations used in this article:

-- Abbreviations used in this article and the LuaSnip docs
local ls = require("luasnip")
local s = ls.snippet
local sn = ls.snippet_node
local t = ls.text_node
local i = ls.insert_node
local f = ls.function_node
local d = ls.dynamic_node
local fmt = require("luasnip.extras.fmt").fmt
local fmta = require("luasnip.extras.fmt").fmta
local rep = require("luasnip.extras").rep

You can find a more complete list in the LuaSnip docs just above the section :help luasnip-basics and (at the time of writing) around line 120 of the source file LuaSnip/lua/luasnip/config.lua.

I’ll use the full names the first few times for the sake of completeness, but will transition to the abbreviations later—just remember that the mysterious-looking ss and ts and is are really just abbreviations of for LuaSnip modules and functions.

Snippet anatomy

Think in terms of nodes: LuaSnip snippets are composed of nodes—think of nodes as building blocks that you put together to make snippets. (Actual node syntax is described soon.) LuaSnip provides around 10 types of nodes. Each node offers a different feature, and your job is to combine these nodes in ways that create useful snippets. (Fortunately, only about 4 nodes are needed for most use cases.)

You create snippets by specifying:

  1. the snippet’s basic parameters (trigger, name, etc.),
  2. the snippet’s nodes, and
  3. possibly some custom expansion conditions and callback functions.

Here is the anatomy of a LuaSnip snippet in code:

-- Anatomy of a LuaSnip snippet
require("luasnip").snippet(
  snip_params:table,  -- table of snippet parameters
  nodes:table,        -- table of snippet nodes
  opts:table|nil      -- *optional* table of additional snippet options
)

And here is an English language summary of the arguments:

  1. snip_params: a table of basic snippet parameters. This is where you put the snippet’s trigger, description, and priority level, autoexpansion policy, and so on.
  2. nodes: a table of nodes making up the snippet (this is the most important part!).
  3. opts: an optional table of additional arguments for more advanced workflows, for example a condition function to implementing custom logic to control snippet expansion or callback functions triggered when navigating through snippet nodes. You’ll leave this optional table blank for simple use cases.

I’ll first cover the snip_params table, then spend most of the remainder of this article explaining various nodes and their use cases.

Setting snippet parameters

TLDR (if you’re familiar with Lua): snip_params is a Lua table; the data type and purpose of each table key is clearly stated in :help luasnip-snippets (just scroll down just a bit). You can now jump to the next section. End TLDR.

And if you’re not yet familiar with Lua tables, you:

Since that might sound vague, here is a concrete example of a “Hello, world!” snippet with a bunch of parameters manually specified, to give you a feel for how this works.

return {
  -- Example: how to set snippet parameters
  require("luasnip").snippet(
    { -- Table 1: snippet parameters
      trig="hi",
      dscr="An autotriggering snippet that expands 'hi' into 'Hello, world!'",
      regTrig=false,
      priority=100,
      snippetType="autosnippet"
    },
    { -- Table 2: snippet nodes (don't worry about this for now---we'll cover nodes shortly)
      t("Hello, world!"), -- A single text node
    }
    -- Table 3, the advanced snippet options, is left blank.
  ),
}

This snippet expands the trigger string "hi" into the string "Hello, world!"; we have given the snippet a human-readable description (with dscr), explicitly specified that the trigger is not a Lua regular expression (with regTrig=false), lowered the snippet’s priority to 100 (the default is 1000), and made the snippet autoexpand by setting snippetType="autosnippet".

Don’t worry about the t("Hello, world!") part for now—this is a text node, which we’ll cover shortly. Note also that I’ve left out the optional third table of advanced options—it’s not needed here.

You should probably read through :help luasnip-snippets to see the full list of table parameter keys (e.g. trig, dscr, etc.). You usually only use a few keys and leave the rest with their default values; we’ll only need the following parameters in this guide:

A common shortcut you’ll see in the wild

The trig key is the only required snippet key, and if you only need to set trig and leave the other keys with their default values, you can use the following shorthand syntax:

return {
  -- Shorthand example: the same snippet as above, but only setting the `trig` param
  s("hi", -- the snip_param table is replaced by a single string holding `trig`
    { -- Table 2: snippet nodes
      t("Hello, world!"),
    }
  ),
}

Explanation: notice that the snip_param table of snippet parameters is now gone—if you only need to set the trig key, you can optionally replace the parameter table with a single string, and LuaSnip will interpret this string as the value of the trig key. You’ll see this syntax a lot in the LuaSnip docs and on the Internet, so I wanted to show it here, but in this article I’ll always explicitly specify the trig key and use a parameter table, which I think is clearer for new users.

That’s all for setting snippet parameters—let’s write some actual snippets!

Actually writing snippets

Goal of this section: explain enough syntax to cover what a typical user will ever need from LuaSnip; after reading it you should have all the tools you need to set up an efficient LaTeX workflow. Keep in mind that LuaSnip has many power user features not covered in this article, which I leave to more advanced guides.

Text node

Text nodes insert static text into a snippet. Here are typical text node use cases:

You create a text node by passing a string or a table of strings to require("luasnip").text_node() (abbreviated t()). Here is a simple “Hello, world!” example that expands the trigger hi into the string “Hello, world!”:

return {
-- A simple "Hello, world!" text node
s(
  {trig = "hi"}, -- Table of snippet parameters
  { -- Table of snippet nodes
    t("Hello, world!")
  }
),
}

And here are some actual real-life examples I use to easily insert the Greek letter LaTeX commands \alpha, \beta, and \gamma:

return {
-- Examples of Greek letter snippets, autotriggered for efficiency
s({trig=";a", snippetType="autosnippet"},
  {
    t("\\alpha"),
  }
),
s({trig=";b", snippetType="autosnippet"},
  {
    t("\\beta"),
  }
),
s({trig=";g", snippetType="autosnippet"},
  {
    t("\\gamma"),
  }
),
}

Note that you have to escape the backslash character to insert it literally—for example I have to write t("\\alpha") to produce the string \alpha in the first snippet.

The only other caveat with text nodes is multiline strings: to insert multiple lines with a single text node, write each line as a separate string and wrap the strings in a Lua table. Here is a concrete example of a three-line text node.

return {
-- Example of a multiline text node
s({trig = "lines", dscr = "Demo: a text node with three lines."},
  {
    t({"Line 1", "Line 2", "Line 3"})
  }
),
}

See :help luasnip-textnode for documentation of text nodes.

Insert node

Insert nodes are positions within a snippet at which you can dynamically type text. We’ve seen that text nodes insert static pieces of text—insert nodes allow you to dynamically type whatever text you like. If you are migrating from UltiSnips or SnipMate, LuaSnip insert nodes are analogous to other snippet engines’ tabstops ($1, $2, etc.).

Use case: Combine insert nodes with text nodes to insert variable content (using the insert nodes) into generic surrounding boilerplate (created by the text nodes).

Here are two concrete LaTeX examples of snippets for the LaTeX \texttt and \frac commands—I use text nodes to create the static boilerplate text and place insert nodes between the curly braces to dynamically type the commands’ arguments:

The \texttt and \frac snippets in action

You create an insert node by passing an index number, and optionally some initial text, to require("luasnip").insert_node() (abbreviated i()). Here is the code for the above examples:

return {
-- Combining text and insert nodes to create basic LaTeX commands
s({trig="tt", dscr="Expands 'tt' into '\texttt{}'"},
  {
    t("\\texttt{"), -- remember: backslashes need to be escaped
    i(1),
    t("}"),
  }
),
-- Yes, these jumbles of text nodes and insert nodes get messy fast, and yes,
-- there is a much better, human-readable solution: ls.fmt, described shortly.
s({trig="ff", dscr="Expands 'ff' into '\frac{}{}'"},
  {
    t("\\frac{"),
    i(1),  -- insert node 1
    t("}{"),
    i(2),  -- insert node 2
    t("}")
  }
),
}

Insert node numbering: notice that you can place multiple insert nodes into a snippet (the \frac snippet, for example, has two). You specify the order in which you jump through insert nodes with a natural number (1, 2, 3, etc.) passed to the i() node as a mandatory argument and then navigate forward and backward through the numbered insert nodes by pressing the keys mapped to <Plug>luasnip-jump-next and <Plug>luasnip-jump-prev, respectively (i.e. the keys mapped at the start of this article in the section on snippet trigger and tabstop navigation keys).

See :help luasnip-insertnode for documentation of insert nodes.

Format: a human-friendly syntax for writing snippets

The problem: you’ve probably noticed that combinations of insert nodes and text nodes become hard to read very quickly. Consider, for example, this snippet for a LaTeX equation environment:

return {
-- Example: text and insert nodes quickly become hard to read.
s({trig="eq", dscr="A LaTeX equation environment"},
  {
    t({ -- using a table of strings for multiline text
        "\\begin{equation}",
        "    "
      }),
    i(1),
    t({
        "",
        "\\end{equation}"
      }),
  }
),
}

The above snippet code is not particularly human-readable. The snippet inserts an equation that looks like this…

\begin{equation}
    % Cursor is here
\end{equation}

…but the jumble of text and insert node code does not look like the nicely-indented LaTeX equation environment the code produces. The code is software-friendly (it is relatively easy for LuaSnip to parse) but it is not human-friendly.

LuaSnip solves the human-readability problem with its fmt and fmta functions. These functions give you a clean overview of what the rendered snippet will actually look like—here is the same equation environment snippet written with fmt:

return {
-- The same equation snippet, using LuaSnip's fmt function.
-- The snippet is not shorter, but it is more *human-readable*.
s({trig="eq", dscr="A LaTeX equation environment"},
  fmt( -- The snippet code actually looks like the equation environment it produces.
    [[
      \begin{equation}
          <>
      \end{equation}
    ]],
    -- The insert node is placed in the <> angle brackets
    { i(1) },
    -- This is where I specify that angle brackets are used as node positions.
    { delimiters = "<>" }
  )
),
}

Don’t worry, we’ll break the snippet down piece by piece—I just wanted to first show what the final product looks like.

fmt is a function that returns a table of nodes

LuaSnip’s fmt (the full name is require("luasnip.extras.fmt").fmt) is just a function that returns a table of nodes, and lets you create these nodes in a relatively human-readable way. The point is: although fmt is a new technique, it is not conceptually different from how we’ve been creating snippets so far—it is just another way to supply a snippet with table of nodes.

Here’s the big picture perspective:

-- What we've done so far: write a snippet by specifying node table manaully
require("luasnip").snippet(
  snip_params:table,
  nodes:table,        -- manually specified node table
  opts:table|nil
)

-- Alternative: using the fmt function to create the node table
require("luasnip").snippet(
  snip_params:table,
  fmt(args),          -- fmt returns the node table
  opts:table|nil
)

Using the format function

The fmt function’s call signature looks like this:

-- The fmt function's call signature
fmt(format:string, nodes:table of nodes, fmt_opts:table|nil) -> table of nodes

The fmta function is almost identical to fmtfmt uses {} curly braces as the default node placeholder and fmta uses <> angle brackets (this will make sense in just a moment). The fmta function is more convenient for LaTeX, which itself uses curly braces to specify command and environment arguments, so I’ll mostly use fmta below.

Here’s how to call the fmta function:

  1. Format string: place the snippet’s boilerplate text in a Lua string (you can use quotes for single-line strings and [[]] for multiline strings), and place <> angle brackets at the positions where you want to place insert (or other non-text) nodes. Here are example format strings for the earlier LaTeX snippets:

    -- \texttt snippet
    "\\texttt{<>}"
     
    -- \frac snippet
    "\\frac{<>}{<>}"
     
    -- Equation snippet, using a multiline Lua string.
    -- (No need to escape backslashes in multiline strings.)
    [[
      \begin{equation*}
          <>
      \end{equation*}
    ]]
    

    Escaping delimiters: if you want to insert a delimiter character literally, just repeat it. For example, <<>> would insert literal angle brackets into a fmta string, and {{}} would insert literal curly braces into a fmt string.

  2. Node table: create a Lua table containing one node for each angle bracket placeholder in the boilerplate string. The fmta function will insert the nodes in this table, in sequential order, into the angle bracket placeholders in the boilerplate string. (Examples just below.)

  3. Format options: optionally create a third table of format options with key = value syntax. In practice, you will usually only ever need the delimiter key, which you can use with regular fmt to specify delimiters other than fmt’s default {} curly braces. See the opts entry in :help luasnip-fmt for the full list of possible keys.

Then pass the format string, node table, and optional fmt_opts table (if you’re using one) as arguments to fmt() or fmta(). As always, here are concrete examples—I’ll continue with the \texttt, \frac, and equation snippets.

-- fmta call for the \texttt snippet
fmta(
  "\\texttt{<>}",
  { i(1) },
)

-- Example: using fmt's `delimiters` key to manually specify angle brackets
fmt(
  "\\frac{<>}{<>}",
  {
    i(1),
    i(2)
  },
  {delimiters = "<>"} -- manually specifying angle bracket delimiters
)

-- Using a multiline string for the equation snippet
fmta(
   [[
     \begin{equation*}
         <>
     \end{equation*}
   ]],
   { i(1) }
)

Finally, you create a snippet by using the call to the fmt or fmta function in place of a node table. At the risk of getting boring—I know I’m going slowly here, but I want to fully list all steps—here are the \texttt, \frac, and equation examples as complete snippets.

return {
-- Examples of complete snippets using fmt and fmta

-- \texttt
s({trig="tt", dscr="Expands 'tt' into '\texttt{}'"},
  fmta(
    "\\texttt{<>}",
    { i(1) }
  )
),
-- \frac
s({trig="ff", dscr="Expands 'ff' into '\frac{}{}'"},
  fmt(
    "\\frac{<>}{<>}",
    {
      i(1),
      i(2)
    },
    {delimiters = "<>"} -- manually specifying angle bracket delimiters
  )
),
-- Equation
s({trig="eq", dscr="Expands 'eq' into an equation environment"},
  fmta(
     [[
       \begin{equation*}
           <>
       \end{equation*}
     ]],
     { i(1) }
  )
)
}

See :help luasnip-fmt for complete documentation of fmt and fmta, although the above should have you covered for most use cases.

Insert node tips and tricks

Repeated nodes

Repeated nodes (analogous to what UltiSnips calls mirrored tabstops) allow you to reuse a node’s content in multiple locations throughout the snippet body.

In practice, you might use repeated insert nodes to simultaneously fill out the \begin and \end fields of a LaTeX environment. Here’s an example:

Demonstrating repeated nodes

The syntax for repeated nodes straightforward: you pass the index of the node you want to repeat to a rep() node, which is provided by the luasnip.extras module. For example, here is the code for the snippet shown in the above GIF—note how the rep(1) node in the environment’s \end command repeats the i(1) node in the \begin command.

return {
-- Code for environment snippet in the above GIF
s({trig="env", snippetType="autosnippet"},
  fmta(
    [[
      \begin{<>}
          <>
      \end{<>}
    ]],
    {
      i(1),
      i(2),
      rep(1),  -- this node repeats insert node i(1)
    }
  )
),
}

Note: for text in the repeated node to update as you type (e.g. like in the \end{} field in the above GIF) you should set update_events = 'TextChanged,TextChangedI' in your LuaSnip config. The default update event is InsertLeave, which will update repeated nodes only after leaving insert mode. Repeated nodes are are documented, in passing, in the section :help luasnip-extras.

Custom snippet exit point with the zeroth insert node

By default, you exit/complete a snippet with your cursor placed at the very last piece of text. (In the previous environment snippet, for example, this would be after the \end{} command.) But sometimes it is convenient to complete a snippet with your cursor still inside the snippet body.

You can specify a custom exit point using the zero-index insert node i(0) (which is analogous to $0 in UltiSnips). The i(0) node is always the last node jumped to, and you use it to specify the desired cursor position when the snippet completes. Here is an example where an explicitly-specified i(0) node makes you exit a equation snippet with your cursor conveniently placed inside the environment’s body.

return {
-- Using a zero-index insert node to exit snippet in equation body
s({trig="eq", dscr=""},
  fmta(
    [[
      \begin{equation}
          <>
      \end{equation}
    ]],
    { i(0) }
  )
),
}

If i(0) is not explicitly defined, an i(0) node is implicitly placed at the very end of the snippet—in this case this would be after the \end{equation} command. The zero-index insert node is documented in :help luasnip-insertnode.

Insert node placeholder text

Placeholder text is used to give an insert node a description or default text. You define placeholder text by passing an optional second string argument to an insert node

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:

return {
-- Example use of insert node placeholder text
s({trig="hr", dscr="The hyperref package's href{}{} command (for url links)"},
  fmta(
    [[\href{<>}{<>}]],
    {
      i(1, "url"),
      i(2, "display name"),
    }
  )
),
}

Here is what this snippet looks like in action:

Demonstrating the tabstop placeholder

See the end of :help luasnip-insertnode for documentation of insert node placeholder text.

The visual placeholder and a few advanced nodes

We’ve barely scratched the surface of what LuaSnip can do. Using three nodes called function nodes, dynamic nodes, and snippet nodes, you can create nodes that call custom Lua functions and even recursively return other nodes, which opens up a world of possibilities. This section explains, cookbook-style, how to port an UltiSnips feature called the visual placeholder to LuaSnip.

The visual placeholder lets you use text selected in Vim’s visual mode inside the content of a snippet body. A typical use case is to quickly surround existing text with a snippet (e.g. to surround a word with quotation marks, surround a paragraph in a LaTeX environment, etc.). Here’s a snippet that automatically surrounds selected text in the LaTeX \textit command:

Demonstrating the visual placeholder

What happened: I selected the line of text with V, triggered visual selection with Tab, and after I triggered the snippet (with tii in this case) the \textit{} command’s argument was automatically populated with the previously-selected text.

Here’s how to set up and use visual selection:

Config: visual selection is an opt-in feature; to enable it, open your LuaSnip config and set the store_selection_keys option to the key you want to use to trigger visual selection. The following example uses the Tab key, but you could use any key you like.

-- Somewhere in your Neovim startup, e.g. init.lua
require("luasnip").config.set_config({ -- Setting LuaSnip config
  -- Use <Tab> (or some other key if you prefer) to trigger visual selection
  store_selection_keys = "<Tab>",
})

Pressing <Tab> in visual mode will then store the visually-selected text in a LuaSnip variable called SELECT_RAW, which we will reference later to retrieve the visual selection.

Here’s how to use visual placeholder snippets (it sounds really complicated when written out, but should make more sense in the GIF below and will quickly become part of your muscle memory):

  1. Create and save a LuaSnip snippet with a dynamic node that calls the get_visual function (all of this is described below, with a complete example—I’m just giving an overview for now).
  2. Use Vim to open a file in which you want to test out the just-created snippet.
  3. Use Vim’s visual mode to select some text.
  4. Press the Tab key (or whatever other key you set earlier with store_selection_keys). The selected text is deleted and stored in the LuaSnip variable SELECT_RAW, and you are placed into Vim’s insert mode.
  5. Type the trigger to expand the previously-written snippet that included the dynamic node calling the get_visual function. The snippet expands, and the text you had selected in visual mode and stored in SELECT_RAW appears in place of the dynamic node in the snippet body.

Here’s the above GIF again—see if you can identify steps 3 (V), 4 (Tab), and 5 (trigger):

Demonstrating the visual placeholder

Here is the corresponding snippet code:

-- This is the `get_visual` function I've been talking about.
-- ----------------------------------------------------------------------------
-- Summary: If `SELECT_RAW` is populated with a visual selection, the function
-- returns an insert node whose initial text is set to the visual selection.
-- If `SELECT_RAW` is empty, the function simply returns an empty insert node.
local get_visual = function(args, parent)
  if (#parent.snippet.env.SELECT_RAW > 0) then
    return sn(nil, i(1, parent.snippet.env.SELECT_RAW))
  else  -- If SELECT_RAW is empty, return a blank insert node
    return sn(nil, i(1))
  end
end
-- ----------------------------------------------------------------------------

return {
-- Example: italic font implementing visual selection
s({trig = "tii", dscr = "Expands 'tii' into LaTeX's textit{} command."},
  fmta("\\textit{<>}",
    {
      d(1, get_visual),
    }
  )
),
}

A few comments:

Here’s the great thing: you can still use any snippet that includes the d(1, get_visual) dynamic node without going through the select-and-Tab procedure described above—if there is no active visual selection, the dynamic node simply acts as a regular insert node.

Docs: This use of dynamic nodes and SELECT_RAW to create a visual-selection snippet is not explicitly mentioned in the LuaSnip docs at the time of writing, but you can read about SELECT_RAW at :help luasnip-variables and about dynamic nodes, as mentioned earlier, at :help luasnip-dynamicnode. The store_selection_keys config key is documented in the LuaSnip README’s config section.

Conditional snippet expansion

The problem and the solution

If you haven’t noticed already, sooner or later you’ll run into the following problem:

Short, easy-to-type snippet triggers tend to interfere with words typed in regular text.

This problem becomes particularly noticeable if you use autotrigger snippets, (which I strongly encourage if you need to type LaTeX quickly and conveniently). For example:

You get the idea—loosely, we need a way to “stop snippets from expanding when we don’t want them to”. This section gives two solutions to this problem:

  1. Regular expansion (regex) triggers
  2. Making certain snippets expand only when the trigger is typed in certain LaTeX contexts (e.g. math, comments, only in a specific environment, etc.)

In combination, these techniques should solve your snippet expansion problems in all typical use cases. I’ll cover regex triggers first, since they apply to any filetype workflow, and then cover math-specific and environment-specific expansion, which are more LaTeX-specific.

Regex snippet triggers

For our purposes, if you aren’t familiar with them, regular expressions let you implement conditional pattern matching in snippet triggers. You could use a regular expression trigger, for example, to do something like “make the trigger ff expand to the fraction snippet \frac{i(1)}{i(2)}, but only if the ff does not come after an alphabetical character”. (That would solve the problem of ff expanding in words like “off” or “offer”.)

Technicality: Lua patterns vs. traditional regexps: the Lua language, and thus LuaSnip, uses a flavor of regular expressions called “Lua patterns”, which basically provide a simple, limited subset of what “traditional” (e.g. POSIX or Perl) regular expressions can do. If you’re already familiar with traditional regex syntax, Lua patterns will be easy for you—for our purposes, the only meaningful difference is that Lua patterns use the percent sign instead of the backslash to escape characters. I’ll use the terms “regex” and “Lua pattern” interchangeably in this article.

A formal explanation of regular expressions and Lua patterns falls beyond the scope of this article, 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 first watching Corey Schafer’s YouTube tutorial on traditional regexes, then reading the Programming in Lua book’s section on Lua patterns.

For future reference, here the Lua pattern keywords needed for this article:

Keyword Matched characters
. all characters
%d digits
%a letters (uppercase and lowercase)
%w alphanumeric characters
%s white space characters

Here’s how the following sections will work:

Suppress expansion after alphanumeric characters.

The following trigger expands after blank spaces, punctuation marks, braces and other delimiters, but not after alphanumeric characters. Here are the snippet parameter tables for a few variations on the same theme:

-- Prevents expansion if 'foo' is typed after letters
 s({trig = "([^%a])foo", regTrig = true, wordTrig = false}

-- Prevents expansion if 'foo' is typed after alphanumeric characters
 s({trig = "([^%w])foo", regTrig = true, wordTrig = false}

Explanation: %a represents letters; the ^ character, when used in square brackets, performs negation, so [^%a]foo will negate (reject) all matches when foo is typed after a letter; and ([^%a]) captures matched non-letter characters to insert back into the snippet body. (You get behavior similar to this out of the box from LuaSnip’s default wordTrig snippet parameter (mentioned in :help luasnip-snippets), but I prefer custom regex triggers for finer-grained control, so I’ve set wordTrig = false and will continue to do so in the remaining regex snippets.)

This is by far my most-used class of regex triggers, because it prevents common snippet triggers from expanding in regular words. Here are some example use cases:

Intermezzo: function nodes and regex captures

TLDR: Saw those weird-looking function nodes f( function(_, snip) return snip.captures[1] end ) popping up in the above regex-triggered snippets? This node just inserts regex capture groups from snippet’s trigger back into the snippet body. You can now jump to the next section. End TLDR.

Longer explanation: regex-triggered snippets have a potential problem. In the fraction snippet, for example, the entire pattern "([^%a])ff" (including leading non-letter character ([^%a])) is interpreted as the snippet trigger, not just the string "ff", and so the leading non-letter character is never inserted back into the snippet and disappears! This might sound vague, but try copying and triggering the above regex snippets and notice how after expansion the character before the trigger disappears.

The solution is to access the leading whitespace from the trigger’s regex capture group and insert it back into the snippet. You can access regex capture groups with LuaSnip function nodes—the syntax looks like this…

-- Accessing regex capture groups with LuaSnip
f( function(_, snip) return snip.captures[1] end ) -- return first capture group
f( function(_, snip) return snip.captures[2] end ) -- return second capture group, etc.

…and that is why each snippet above included a function node. It’s bit verbose, to be sure, but in practice you basically only write the capture group function node once and then copy and paste it into your other snippets, so it’s not too bad.

Expand only after alphanumeric characters and closing delimiters

This class of triggers expands only after letter characters and closing delimiters, but not after blank spaces or numbers.

-- Only after letters
s({trig = '([%a])foo', regTrig = true, wordTrig = false}

-- Only after letters and closing delimiters
s({trig = '([%a%)%]%}])foo', regTrig = true, wordTrig = false}

Explanation: %a matches letters; %), %], and %} match closing parentheses, square brackets, and curly braces, respectively (these three characters have to be escaped with the percent sign); and ([%a%)%]%}]) saves the captured characters in a capture group.

I don’t use this trigger that 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:

return {
-- A fun zero subscript snippet
s({trig = '([%a%)%]%}])00', regTrig = true, wordTrig = false, snippetType="autosnippet"},
  fmta(
    "<>_{<>}",
    {
      f( function(_, snip) return snip.captures[1] end ),
      t("0")
    }
  )
),
}

And here is the above snippet in action:

The 0 subscript snippet in action

Combined with math-context expansion (described below), these three classes of regex triggers cover the majority of my use cases and should give you enough tools to get started writing your own snippets.

Bonus: expansion only at the start of a new line

This is the equivalent of the UltiSnips b option, and will only expand snippets on new lines. This trigger is useful for expanding environments, \section-style commands, preamble commands, which are usually defined only on new lines.

You could do this manually with a regex trigger like "^([%s]*)foo", but LuaSnip provides a cleaner way to do this—a built-in line_begin expansion condition. This will be our first use of the condition key in a LuaSnip snippet’s opts table (mentioned in the snippet anatomy section earlier in this article and documented towards the bottom of :help luasnip-snippets)—here are a few real-life examples of how to use it: one uses the HTML-inspired trigger h1 to create LaTeX \section commands (you could use h2 for \subsection, and so on), and the other uses new to create a new environment.

-- Example: expanding a snippet on a new line only.
-- In a snippet file, first require the line_begin condition...
local line_begin = require("luasnip.extras.expand_conditions").line_begin

-- ...then add `condition=line_begin` to any snippet's `opts` table:
return {
s({trig = "h1", dscr="Top-level section"},
  fmta(
    [[\section{<>}]],
    { i(1) }
  ), 
  {condition = line_begin}  -- set condition in the `opts` table
),

s({trig="new", dscr="A generic new environmennt"},
  fmta(
    [[
      \begin{<>}
          <>
      \end{<>}
    ]],
    {
      i(1),
      i(2),
      rep(1),
    }
  ),
  {condition = line_begin}
),
}

Context-specific expansion for LaTeX

The condition option in a LuaSnip snippet’s opts table gives you essentially arbitrary control over when snippets expand. We used it above to implement the line_begin expansion condition; here’s how to use it more generally:

  1. In a snippet file, write a Lua function that returns a boolean value: true when a snippet should expand and false when it should not. Here is a silly example that uses Vim’s line() function (documented at :help line()) and the Lua modulo operator to only expand snippets on even-numbered lines.

    -- Silly example: returns true when the cursor is on an even-numbered line
    is_even_line = function()
      local line_number = vim.fn['line']('.')
      if ((line_number % 2) == 0) then  -- an even-numbered line
        return true
      else  -- an odd-numbered line
        return false
      end
    end
    -- (Yes, I know I could have written `return ((line_number % 2) == 0)`,
    -- but I wanted to make the if/else logic explicitly clear.)
    
  2. Set the condition key in a snippet’s opts table to the name of the expansion function:

    return {
    s({trig="test", snippetType="autosnippet"},
       {t("The current line number is even")},
       {condition = is_even_line}
    ),
    }
    

    The above snippet will expand only on even lines (just make sure to include the is_even_line function in the snippet file).

The condition key gives you a lot of power, especially if you leverage built-in Vim functions (e.g. line(), col(), nvim_get_current_line(), etc.) to get information about the current line and cursor position for use in the condition function. LuaSnip even passes a few convenience variables to the condition function for you—see the opts section in :help luasnip-snippets for details.

To implement math-specific snippet expansion, you basically need a function that returns true in math contexts and false otherwise. The excellent VimTeX plugin provides exactly such a function—the in_mathzone() function in vimtex/autoload/vimtex/syntax.vim. You can integrate VimTeX’s math zone detection with LuaSnip’s condition feature as follows:

-- Include this `in_mathzone` function at the start of a snippets file...
local in_mathzone = function()
  -- The `in_mathzone` function requires the VimTeX plugin
  return vim.fn['vimtex#syntax#in_mathzone']() == 1
end
-- Then pass the table `{condition = in_mathzone}` to any snippet you want to
-- expand only in math contexts.

return {
-- Another take on the fraction snippet without using a regex trigger
s({trig = "ff"},
  fmta(
    "\\frac{<>}{<>}",
    {
      i(1),
      i(2),
    }
  ),
  {condition = in_mathzone}  -- `condition` option passed in the snippet `opts` table 
),
}

You can use analogous expansion functions for any other LaTeX context, as long as you have a function that reliably detects if the cursor is currently in a given context or not, where VimTeX again comes to the rescue. Following are a few more examples for conditional expansion in comments, text, and various LaTeX environments—I’ve wrapped the various condition functions in a Lua table called tex_utils for organizational purposes.

Here are the expansion functions…

-- Some LaTeX-specific conditional expansion functions (requires VimTeX)

local tex_utils = {}
tex_utils.in_mathzone = function()  -- math context detection
  return vim.fn['vimtex#syntax#in_mathzone']() == 1
end
tex_utils.in_text = function()
  return not tex_utils.in_mathzone()
end
tex_utils.in_comment = function()  -- comment detection
  return vim.fn['vimtex#syntax#in_comment']() == 1
end
tex_utils.in_env = function(name)  -- generic environment detection
    local is_inside = vim.fn['vimtex#env#is_inside'](name)
    return (is_inside[1] > 0 and is_inside[2] > 0)
end
-- A few concrete environments---adapt as needed
tex_utils.in_equation = function()  -- equation environment detection
    return tex_utils.in_env('equation')
end
tex_utils.in_itemize = function()  -- itemize environment detection
    return tex_utils.in_env('itemize')
end
tex_utils.in_tikz = function()  -- TikZ picture environment detection
    return tex_utils.in_env('tikzpicture')
end

…and here is a simple example: expanding dd into the TikZ \draw command only in tikzpicture environments—you can of course use any condition you like in your own snippets.

return {
-- Expand 'dd' into \draw, but only in TikZ environments
s({trig = "dd"},
  fmta(
    "\\draw [<>] ",
    {
      i(1, "params"),
    }
  ),
  { condition = tex_utils.in_tikz }
),
}

As always, make sure to define the conditional expansion functions in any snippet file you wish to use them in!

Acknowledgements: thank you to @evesdropper and @lervag for the good ideas and discussion in VimTeX issue #2501, which is where I got the idea for environment-specific expansion; my original source for math-context expansion is the famous Gilles Castel article.

Extra

(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: Refreshing snippets from a separate Vim instance

In addition to initially loading snippets, the Lua loader functions load and lazy_load (covered at the start of this article) will refresh the snippets in the current Vim instance to reflect the contents of your snippets directory. Here’s an example use case:

Since this workflow comes up regularly if you use snippets often, and the above command is inconvenient to type manually, and I suggest writing a key mapping to do it for you. The following mapping, for example, makes <Leader>L reload your LuaSnip snippets.

-- In Lua
vim.keymap.set('n', '<Leader>L', '<Cmd>lua require("luasnip.loaders.from_lua").load({paths = "~/.config/nvim/LuaSnip/"})<CR>')
" In Vimscript
nnoremap <leader>L <Cmd>lua require("luasnip.loaders.from_lua").load({paths = "~/.config/nvim/LuaSnip/"})<CR>

Of course, if needed, you should update ~/.config/nvim/LuaSnip/ to your own snippet directory, covered at the start of this article.

In case they look unfamiliar, the above code snippets are Vim key mappings, a standard Vim configuration tool described in much more detail in the series’s final article, 7. A Vimscript Primer for Filetype-Specific Workflows.

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