2. A LuaSnip guide for LaTeX workflows
You might be interested in this article for two reasons:
- As a standalone guide to writing snippets with the LuaSnip plugin (this article is LaTeX-heavy, but it is applicable to any language).
- 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:
- A short intro to snippets for people reading along with the LaTeX series
- A TLDR example of hello world with LuaSnip
- Boring house-keeping stuff (installation, configuration, file organization, snippet syntax etc.)
- Actually writing snippets—you probably came for this section.
- 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
- TLDR hello world example
- Getting started with LuaSnip
- Snippet files, directories, and loaders
- Snippet anatomy
- Actually writing snippets
- Conditional snippet expansion
- Extra
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.
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:
-
Install LuaSnip (I’m assuming you’re comfortable installing Vim plugins.)
-
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>'
-
Create a snippets directory at
${HOME}/.config/nvim/LuaSnip/
, inside it create an empty snippets file calledall.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.") } ), }
-
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/"})
-
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). -
Enter insert mode and write
hi
(because we wrote a snippet with triggertrig="hi"
). With your cursor at the end ofhi
, press the Tab key (because the Tab key was mapped to snippet expansion a few steps up). The wordhi
should expand intoHello, 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?
- Vim users: use UltiSnips—LuaSnip only works with Neovim
- Neovim users: I suggest LuaSnip—it integrates better into the Neovim ecosystem, is free of external dependencies (UltiSnips requires Python), has more features, and is a bit faster (no, I don’t have benchmarks). That said, UltiSnips still works fine in Neovim, and its syntax is easier to learn.
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:
- where the text files holding your snippets are stored on your local file system, and
- 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:
- the key you use to trigger (expand) snippets,
- the key you use to jump forward through a snippet’s tabstops, and
- 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:
-
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. -
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)
-
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 tovim.cmd
, e.g.-- Any Lua config file, e.g. init.lua vim.cmd[[ " Vimscript goes here! ]]
If needed, see
:help vim.cmd()
for details. -
In case it’s unfamiliar, the conditional ternary operator
condition ? expr1 : expr2
executesexpr1
ifcondition
is true and executesexpr2
ifcondition
is false—it is common in C and many other languages. In the firstimap
mapping, for example, the ternary operator is used to map<Tab>
to<Plug>luasnip-expand-or-jump
ifluasnip#expand_or_jumpable()
returnstrue
and to<Tab>
ifluasnip#expand_or_jumpable()
returnsfalse
. -
You need to apply tabstop navigation in both insert and visual modes, hence the use of both
imap
andsmap
for the forward and backward jump mappings. (Well, technically select mode and not visual mode, hence the use ofsmap
and notvmap
, but for a typical end user’s purposes select and visual mode look identical. See:help select-mode
for details.)Also,
imap
andsmap
are intentionally used instead ofinoremap
andsnoremap
—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. -
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 thejumpable(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:
- Covered in this article: Native LuaSnip snippets written in Lua (support for all LuaSnip features, best integration with the larger Neovim ecosystem).
- 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:
- People seem to have the most trouble with native LuaSnip syntax, so covering it should benefit the most people.
- Native LuaSnip snippets give you more features, and integrate a bit better into the larger Neovim ecosystem (e.g. Tree-sitter and Telescope), than imported third-party snippets.
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:
-
Covered in this article: write Lua snippets in dedicated snippet files and load these files using LuaSnip’s Lua loader feature.
-
Define and load snippets in your Neovim startup files using LuaSnip’s
add_snippets
function.
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:
-
Write LuaSnip snippets in plain-text Lua files with the
.lua
extension. (Snippet syntax is described soon.) -
Wrap all snippets in a given
.lua
file inside a Lua table, andreturn
this table from the Lua file. (Examples follow.) -
Name each snippet file appropriately: The file’s base name determines which Vim
filetype
the snippets apply to. For example, snippets inside the filetex.lua
would apply to files withfiletype=tex
, snippets insidehtml.lua
to files withfiletype=html
, and so on.If you want certain snippets to apply globally to all file types, place these global snippets in the file
all.lua
. (This is the same naming scheme used by UltiSnips, in case you are migrating from UltiSnips). -
By default, LuaSnip expects your snippets to live in directories called
luasnippets
placed anywhere in your Neovimruntimepath
—this is documented in the description of thepaths
key in:help luasnip-loaders
.However, you can easily override the default
luasnippets
directory name and store snippets in any directory (or set of directories) on your file system—LuaSnip’s loaders let you manually specify the snippet directory path(s) to load. I recommend using a directory in your Neovim config folder, e.g."${HOME}/.config/nvim/LuaSnip/"
. -
Load snippets by calling the LuaSnip Lua loader’s
load
function from somewhere in your Neovim startup config (e.g.init.lua
,init.vim
, etc.):-- Load all snippets from the nvim/LuaSnip directory at startup require("luasnip.loaders.from_lua").load({paths = "~/.config/nvim/LuaSnip/"}) -- Lazy-load snippets, i.e. only load when required, e.g. for a given filetype require("luasnip.loaders.from_lua").lazy_load({paths = "~/.config/nvim/LuaSnip/"})
Bonus: if you manually set the
paths
key when callingload
orlazy_load
, LuaSnip will not need to scan your entire Neovimruntimepath
looking forluasnippets
directories—this should save you a few milliseconds of startup time. -
Want to use multiple snippet directories? No problem—set the
paths
key’s value to a table or comma-separated string of multiple directories. Here are two ways to load snippets from both the directoryLuaSnip1
andLuaSnip2
:-- Two ways to load snippets from both LuaSnip1 and LuaSnip2 -- 1. Using a table require("luasnip.loaders.from_lua").load({paths = {"~/.config/nvim/LuaSnip1/", "~/.config/nvim/LuaSnip2/"}}) -- 2. Using a comma-separated list require("luasnip.loaders.from_lua").load({paths = "~/.config/nvim/LuaSnip1/,~/.config/nvim/LuaSnip2/"})
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 s
s and t
s and i
s 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:
- the snippet’s basic parameters (trigger, name, etc.),
- the snippet’s nodes, and
- 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:
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.nodes
: a table of nodes making up the snippet (this is the most important part!).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:
- define any Lua table, including the
snip_params
table, with{ }
curly braces, - find the list of possible table parameter keys in the LuaSnip docs at
:help luasnip-snippets
, - use
key=value
syntax to set each of the table’s keys, using the possible values listed in:help luasnip-snippets
.
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:
trig
: the string or Lua pattern (i.e. Lua-flavored regex) used to trigger the snippet.regTrig
: whether the snippet trigger should be treated as a Lua pattern. Atrue
/false
boolean value;false
by default.wordTrig
: loosely, a safety feature to prevent snippets from expanding when the snippet trigger is part of a larger word. Atrue
/false
boolean value;true
(enabled) by default. Since thewordTrig
safety feature can conflict with regular expression triggers, you often want to setwordTrig = false
when usingregTrig = true
snippets.snippetType
: either the string"snippet"
(manually triggered) or"autosnippet"
(autotriggered);'snippet'
by default. I encourage autotriggered snippets for efficient typing—see the tips at the bottom of this article for details.
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:
- When used on their own, text nodes can transform a short, easy-to-type trigger into a longer, inconvenient-to-type piece of text.
- When used with other nodes, text nodes provide a template of static boilerplate text into which you dynamically insert variable text with, for example, insert or dynamic nodes.
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:
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 fmt
—fmt
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:
-
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 afmta
string, and{{}}
would insert literal curly braces into afmt
string. -
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.) -
Format options: optionally create a third table of format options with
key = value
syntax. In practice, you will usually only ever need thedelimiter
key, which you can use with regularfmt
to specify delimiters other thanfmt
’s default{}
curly braces. See theopts
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:
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:
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:
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):
- 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). - Use Vim to open a file in which you want to test out the just-created snippet.
- Use Vim’s visual mode to select some text.
- 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 variableSELECT_RAW
, and you are placed into Vim’s insert mode. - 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 inSELECT_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):
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:
- You only need to write the
get_visual
function once per snippet file—you can then use it in all snippets in the file. By the way, there is no need to use the nameget_visual
. You could name the function anything you like. - You’re probably wondering what the heck is a dynamic node—good question.
A full answer falls beyond the scope of this article; see
:help luasnip-dynamicnode
for details. For our purposes, a dynamic node takes a numeric index (just like an insert node) as its first argument and a Lua function as its second argument, and this function (get_visual
in the above example), returns a LuaSnip construct called a snippet node that contains other nodes (a single insert node in the above example). - In the above example the dynamic node has an index of 1, but you can of course set a dynamic node’s index to anything you like if other nodes come earlier.
So, for example, you might first create a snippet that first uses an insert node
i(1)
and only then uses a visual dynamic noded(2, get_visual)
.
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:
ff
is a great choice to trigger a\frac{}{}
snippet—it’s a short, convenient trigger with good semantics—but you wouldn’t wantff
to spontaneously expand to\frac{}{}
in the middle of typing the word “offer” in regular text, for example.mm
is a nice trigger for$ $
(inline math), but expansion would be unacceptable when typing words like “communication”, “command”, etc.
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:
- Regular expansion (regex) triggers
- 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:
- I’ll first give the generic snippet parameter table needed to use each class of regex triggers, and use
foo
as the example trigger. - I’ll give a short explanation of how each Lua regex works.
- I’ll give a few real life examples I personally find useful when writing LaTeX.
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:
-
Make
mm
expand to$ $
(inline math), but not in words like “comment”, “command”, etc…return { s({trig = "([^%a])mm", wordTrig = false, regTrig = true}, fmta( "<>$<>$", { f( function(_, snip) return snip.captures[1] end ), d(1, get_visual), } ) ), }
The
d(1, get_visual)
node implements the visual selection covered earlier in this article. The weird-looking function nodef( function(_, snip) return snip.captures[1] end )
preserves the trigger’s regex capture group and is explained just below. -
Make
ee
expand toe^{}
(Euler’s number raised to a power) after spaces, delimiters, and so on, but not in words like “see”, “feel”, etc…return { s({trig = '([^%a])ee', regTrig = true, wordTrig = false}, fmta( "<>e^{<>}", { f( function(_, snip) return snip.captures[1] end ), d(1, get_visual) } ) ), }
-
Make
ff
expand tofrac{}{}
but not in words like “off”, “offer”, etc…return { s({trig = '([^%a])ff', regTrig = true, wordTrig = false}, fmta( [[<>\frac{<>}{<>}]], { f( function(_, snip) return snip.captures[1] end ), i(1), i(2) } ) ), }
(Note that the
ff -> \frac{}{}
expansion problem can also be solved with a math-context expansion condition, which is covered in the next section.)
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:
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:
-
In a snippet file, write a Lua function that returns a boolean value:
true
when a snippet should expand andfalse
when it should not. Here is a silly example that uses Vim’sline()
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.)
-
Set the
condition
key in a snippet’sopts
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:
-
Use automatic completion whenever possible. This technically makes your snippet engine use more computing resources, but I am yet to notice a perceptible slow-down on modern hardware. For example, I regularly use 150+ autotrigger snippets on a 2.5 GHz, dual-core, third-gen i5 processor and 8 gigabytes of RAM (typical, even modest specs by today’s standards) without any problems.
-
Use short snippet triggers. Like one-, two-, or and maybe three-character triggers.
-
Repeated-character triggers offer a good balance between efficiency and good semantics. For example, I use
ff
(fraction),mm
(inline math), andnn
(new equation environment). Althoughfrac
,$
, andeqn
would be even clearer,ff
,mm
, andnn
still get the message across and are also much faster to type.Use math-context expansion and regular expressions to free up short, convenient triggers that would otherwise conflict with common words.
-
Use ergonomic triggers on or near the home row. Depending on your capacity to develop muscle memory, you can dramatically improve efficiency if you sacrifice meaningful trigger names for convenient trigger locations. I’m talking weird combinations of home row keys like
j
,k
,l
,s
,d
, andf
that smoothly roll off your fingers. For example,sd
,df
,jk
, andkl
, if you can get used to them, are very convenient to type and also don’t conflict with many words in English or Romance languages.Here are two examples I use all the time:
-
I first define the LaTeX command
\newcommand{\diff}{\ensuremath{\operatorname{d}\!}}
in a system-wide preamble file, then access it with the following snippet:return { s({trig = "df", snippetType = "autosnippet"}, { t("\\diff") }, { condition = tex.in_mathzone } ), }
This
df
snippet makes typing differentials a breeze, with correct spacing, upright font, and all that. Happily, in this case usingdf
for a differential also makes semantic sense.You can see the
\diff
snippet playing a minor supporting role as the differential in this variation of the fundamental theorem of calculus:As a side note, using a
\diff
command also makes redefinition of the differential symbol very easy—for example to adapt an article for submission to a journal that uses italic instead of upright differentials, one could just replace\operatorname{d}\!
with\,d
in the command definition instead of rummaging through LaTeX source code changing individual differentials. -
I use the following snippet for upright text in subscripts—the trigger makes no semantic sense, but I got used to it and love it.
return { s({trig = 'sd', snippetType="autosnippet", wordTrig=false}, fmta("_{\\mathrm{<>}}", { d(1, get_visual) } ), {condition = tex.in_mathzone} ), }
This snippet triggers in math contexts and includes a visual placeholder.
Please keep in mind: I’m not suggesting you should stop what you’re doing, fire up your Vim config, and start using
sd
to trigger upright-text subscripts just like me. The point here is just to get you thinking about using the home-row keys as efficient snippet triggers. Try experimenting for yourself—you might significantly speed up your editing. Or maybe this tip doesn’t work for you, and that’s fine, too.
-
-
Try using
jk
as your<Plug>luasnip-jump-next
key, i.e. for jumping forward through tabstops:imap <silent><expr> jk luasnip#jumpable(1) ? '<Plug>luasnip-jump-next' : 'jk' smap <silent><expr> jk luasnip#jumpable(1) ? '<Plug>luasnip-jump-next' : 'jk'
The other obvious choice is the Tab key, but I found the resulting pinky reach away from the home row to be a hindrance in real-time LaTeX editing. Of course
jk
is two key presses instead of one, but it rolls of the fingers so quickly that I don’t notice a slowdown. (And you don’t havejk
reserved for exiting Vim’s insert mode because you’ve remapped Caps Lock to Escape on a system-wide level and use that to exit insert mode, right?)
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:
-
Problem: you’re editing
foobar.tex
in one Vim instance, make some changes to the snippets filetex.lua
in a separate Vim instance, and want the updates to be immediately available infoobar.tex
without having to restart Vim. (Any snippet edits made in the current Vim instance should already be automatically available.) -
Solution: call Lua loader function with the Vim command
lua require("luasnip.loaders.from_lua").load({paths = "~/.config/nvim/LuaSnip/"})<CR>
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.