This is a guide centered around moving snippets from UltiSnips to LuaSnip. While a majority of snippets discussed will be LaTeX snippets, I will not be discussing practices for creating LaTeX snippets or anything of the like - that seems better suited for a future guide.
I’d also like to thank the following people for providing help:
These users’ help on figuring out how to start with LuaSnip as well as a group effort to figure out how to create conditional snippets using VimTeX syntax highlighting has been incredibly crucial for this guide.
UltiSnips is one of the most well-known snippet engines, and it’s what I was introduced to and often used as I started my Neovim + LaTeX journey. However, as time went on, I decided to switch to a more Lua-based configuration, and UltiSnips, while trusty, was in need of an upgrade. That’s when I found LuaSnip.
However, unlike UltiSnips, which was fairly easy to set up within a day, LuaSnip had me incredibly confused - after reading the docs and watching videos, I was still lost. But through some community help and more research, I was able to successfully convert most of my snippets from UltiSnips to LuaSnip while adding on even more functionality, and my LaTeX typesetting experience has never been better.
So here’s a summary of my journey through moving to LuaSnip. With this article, I hope to provide a more detailed written guide/framework for creating snippets in LuaSnip. While the main focus is on moving snippets from UltiSnips to LuaSnip (mostly with LaTeX), those with little to no experience in creating snippets and/or LaTeX typesetting may find this guide helpful as well.
Why not start off with a small introduction of LuaSnip and UltiSnips? LuaSnip, like UltiSnips, is a snippet plugin for (Neo)vim which allows for faster code writing. However, the two have a few main differences.
While UltiSnips is quite simple in format, only using plain text and dollar signs ($0
, $1
) to denote inputs, LuaSnip opts for different “nodes”, making the format more verbose. (You’ll later find out that this can be remedied with a format add-on, but for most users - myself included, seeing nodes might be a little off-putting and perhaps even confusing at first.)
And secondly, as the name implies, LuaSnip is heavily based on Lua parsing, whereas UltiSnips uses Python. For the most part, there is very little difference between the two save for a few syntax changes. However, the Achilles’ heel of Lua parsing is probably its more limited regex parsing - Lua only supports its own Lua patterns, which are not as complete as Python’s full suite of regex features. The lack of such regex support may make a few commonly defined snippets in UltiSnips a bit harder to convert, but in general, LuaSnip should be able to replicate most, if not all functionality of UltiSnips and add its own flair. Here’s a reference of all the snippet types/conditions I commonly used in UltiSnips and their LuaSnip counterparts as a reference and a preview as to what’s next:
UltiSnips | LuaSnip |
---|---|
a Snippets | autoexpand = true , put on separate snip table or autosnippet |
b Snippets | require("luasnip.extras.expand_conditions").line_begin |
i /w Snippets | wordTrig = (true|false) Set to true by default |
r Snippets | regTrig = true |
Nested Triggers | Choice Nodes |
e /context Snippets | LuaSnip conditions |
Visual Mode Snippets | LS_SELECT_RAW/LS_SELECT_DEDENT |
Autoexpand/parsing heavy snippets | Function/Choice/Dynamic Nodes |
Now, let’s get started by setting up LuaSnip.
Setting up LuaSnip is a short process - you can get your snippets going in no time. Start by installing the plugin:
|
|
Then, set up LuaSnip. I have the following options on:
autosnippets = true
for automatically triggering snippets.history = true
to keep around the last snippet to jump back easily.~/.config/nvim/lua/snippets/
. You can also LuaSnip parse snippets from other engines, such as VSCode and Snipmate, but I will not be covering that.Here’s my setup configuration:
|
|
Feel free to adjust the configuration to your needs by taking a look at official documentation.
Of course, make sure to set up some keybinds so that you can quickly navigate through snippets. A common choice is to use something like <Tab>
and <S-Tab>
to go forwards and backwards respectively.
|
|
Sourced from the plugin README.md; I have something different set up with
nvim-cmp
.
Now let’s get ready to write snippets. Navigate to the folder you’re sourcing your snippets from; to create snippets of a certain filetype, open <filetype>.lua
, or use all.lua
for global snippets. As I use LaTeX snippets, I’ll be using tex.lua
. Now, let’s learn about snippets.
A snippet consists of three parts:
Combining each of the separate parts, a snippet should look somewhat like this:
|
|
This should provide enough background information to get started with creating a snippet of your own.
For LaTeX typesetting, text is often enclosed in environments, so a handy snippet is something that quickly inserts an environment. In UltiSnips, it is defined as the following:
|
|
Let’s try to replicate that with LuaSnip. A possible solution looks to be the following:
|
|
But if you go and try it out, it doesn’t work and the formatting is all messy. This is because of two things:
i(1)
nodes confuses LuaSnip.This can be easily remedied by introducing the fmt
utility and the repeat node. The fmt
utility is used in the nodes slot, changing the initial snippet format to something like this:
|
|
Note:
fmta
is another option - it’s just likefmt
but sets default delimiters to angled brackets, which is preferable if you have a language that uses a lot of curly braces (like LaTeX).
As for the repeat node, it should be pretty self-explanatory: rep(<num>)
repeats the node specified. With this, we have our new snippet:
|
|
Well, that was a lot of work to write a first snippet. Writing dozens of different snippets must take a lot of time. Not if you use snippets - yep, snippets to generate snippets. With this current knowledge, it’s enough to make something that might be helpful to generating snippets. Two of my snippets to expedite snippet creation are the following:
|
|
The current version adds a choice node feature; we’ll get to that later on in this guide. You can also find more snippet-creating snippets in my Lua snippets file.
Even cooler: As an advanced setup, you can use functions in Lua (article coming soon!) to generate snippets with very similar structures. Source and Inspiration: here.
With the knowledge of text, insert, and repeat nodes as well as the snippet-creating snippets to speed up the snippet-writing process, you should be able to quickly implement a majority of the snippets you will ever want to use.
It’s time to introduce more snippet types and create more complicated snippets. Here we’ll discuss Regex, Function Nodes, Choice Nodes, Dynamic Nodes, and Conditions.
A cool snippet and a huge time saver for those who do math work is the auto subscript snippet from UltiSnips, turning something like x1
to x_1
and x_1
to x_{10}
.
Here’s how it can be implemented in UltiSnips:
|
|
Looking at the snippet signature, it requires word triggers and regex triggers. Word trigger is built-in for LuaSnip, and using regex is almost as simple - simply append regTrig=true
to the trigger table, and use a Lua pattern style trigger.
What about the next lines - returning the matched text back and editing the snippet? As mentioned previously, this was done with Python parsing in UltiSnips, but now is done with Lua parsing in LuaSnip. However, recall LuaSnip has a lot of snippet types - and here, we’re introducing one that specifically deals with parsing - the function node.
For the most part, parsing snippets usually rely on captured values as arguments that are passed in or something that is saved and returned in the snippet output, which can be accessed through the snip.captures
argument. Each of the captures in parentheses is like a Python match group and can be accessed as if they were in a Lua table (which unfortunately happens to use 1-indexing like MATLAB).
This gives our function nodes some general format like so:
|
|
For our auto subscript function, all we want to do is hold on to the values, so we just return snip.captures[1]
and snip.captures[2]
as needed. Here’s the final implementation:
|
|
For certain snippets, LuaSnip has some perfect edge case functions in postfix snippets and lambdas - previously in UltiSnips, this was done with regex triggers and a lot of parsing. For example, appending hats to symbols can be done with ease like so:
|
|
For very simple snippets, a one-line postfix solution may be better than the snip.captures
option and a function node.
LS_SELECT_RAW/LS_SELECT_DEDENT
: Visual Mode SnippetsA less commonly used set of snippets are those in Visual mode. Usually they look something like this in UltiSnips:
|
|
This similar behavior can be replicated in LuaSnip using the LS_SELECT_RAW/LS_SELECT_DEDENT
variables. To have this work, make sure you have something like store_selection_keys="<Tab>"
somewhere in your config to store the values. After your configuration is set up, it’s time to create the snippet.
The idea behind this is very similar - use LS_SELECT_RAW
, but instead of directly using the snippet, use the store_selection_keys
trigger to save it to the LS_SELECT_RAW
variable, then apply the snippet. And how do we get the values from LS_SELECT_RAW
to show up with the snippet? This is done with a function node that retrieves the values we want. Here’s the finished snippet:
|
|
There isn’t much that parallels with UltiSnips in this section, but nevertheless, this is a great utility to learn and implement.
With choice nodes and the next topic, dynamic nodes, we start moving away from simple inputs and towards even more complex functions. Through this, the extensibility and customizability of LuaSnip really starts to shine. However, the more complex and awe-inducing the plugin gets, the harder it gets to understand - because these nodes lack concrete output, it can be difficult to visualize and experiment with. Fortunately, for most use cases, everything from before should be more than sufficient, but it’s nice to learn more and have some truly powerful snippets.
Let’s focus on choice nodes. As the name suggests, this node allows you to select between a list of nodes. Of course, you can just use text nodes to emulate stationary values, but with the option to include more complex nodes - the possibilities are endless.
A simple example is the usage of choice nodes is a snippet that toggles between different delimiters with minted/lstlistings
code listings. If you want to use code highlighting in LaTeX, trying to highlight something like {code}
is a hassle as LaTeX ends the command with the bracket delimiter, so in those cases, it might be wise to switch over to the upright delimiter.
The main part of the snippet is in the choice node - let’s define our choices:
{}
and insert code in the middle.||
and insert code in the middle.Now, to execute our choices, we’ll need to know a bit more about the snippet node.
Snippet nodes are pretty crucial in choice nodes and the next topic, dynamic nodes. While the concept may seem a bit strange at first, it might be easier to think of a snippet as a “nested snippet” or a way to express multiple inputs with one node. For instance, in the above choices, a choice node provides us one space for an action (before the comma separation pushes us to the next choice), but we have three snippet actions: add opening delimiter as text, add code listing as input, add closing delimiter as text. The solution is to nest them all in a snippet node which allows all three actions to be processed with one choice.
A snippet node has the following formatting: sn(index, {nodes})
. Typically in choice (and dynamic) nodes, the jump index will be nil
as it is often nested within a function/choice.
Now that we have our choices and a way to execute them using the snippet node, the snippet comes together pretty quickly:
|
|
Along with being an incredibly useful side tool, choice nodes also introduce modularity into snippets, which allows them to adapt to your needs. Previously, in UltiSnips, to implement some form of modularity, it would make sense to make one more general snippet (e.g. one for the figure
environment), and a more specific snippet to be inputted inside the more general snippet (e.g. tikzpicture
or \includegraphics
defaults).
Although I don’t have that snippet written out right now, another nice example is a snippet I showcased earlier - the snippet to make a short text snippet. For some snippets, I might only need the trigger keyword to initialize a snippet, while other times, I might want to add other information to the trigger table, such as snippet priority values to prevent the snippet from improperly triggering.
As with the previous snippet, we have the main snippet body and the choice:
<>(<>, {t('<>')}<> <>)<>,
, where each <>
denotes the following: snippet type (tab-triggered or autosnippet), choice of trigger or trigger table, text entry, options.{trig=<>, <>}
, where the <>
denote the trigger word and other possible trigger options we might want. This is done through a combination of text and insert nodes, which can be implemented with a snippet node.Combining the choices with the snippet setup, we have our finished snippet here:
|
|
If you want to use choice nodes often, make sure to include this in your config for quick and easy mapping.
|
|
We’ve made it to the grand finale of the snippet node types: the dynamic nodes. In essence, these powerful nodes allow for custom snippet node return values (think text/insert nodes depending on user input), building upon function nodes (limited to string nodes), and choice nodes (limited to only the choices defined by the user) to offer a more generalized output. Let’s get acquainted with the dynamic node by constructing table rows based on user input, then generating tables and matrices.
A constant complaint of LaTeX users is generating something like tables and matrices, which can often be very tedious to typeset by hand. Previously, this was done using auto-expand snippets with UltiSnips, but what about with LuaSnip?
|
|
Well, as this suggests, we can use Dynamic Nodes. But what are dynamic nodes? These are the most powerful nodes that can exist in LuaSnip. While other nodes usually have set-in-stone outputs (e.g. strings, function outputs), dynamic nodes return snippet nodes, which is basically another snippet. This allows us to create snippets that depend on user input, such as tables and matrices. For reference, here’s a nice way I can think of function nodes as opposed to dynamic nodes.
|
|
To create the matrix, the dynamic node comes into play as we construct the body of the matrix. A basic matrix in LaTeX looks something like this:
|
|
The focus is on the body of the matrix, which requires an insert node at each letter and the bordering text (&
between entries and \\
at the end). As for rows and columns, this is determined by user input (at least for my snippet - plugin creator L3MON4D3 uses a different dynamic scheme). Since we now the nodes we need to place and where to input them, creating a dynamic node becomes easier.
With this, we can yank code from L3MON4D3’s example in the wiki and modify it slightly construct something based on the Python code to generate the matrix body. Here’s the finished product:
|
|
In essence, dynamic nodes and pre-expand snippets are quite similar, at least in this case - while the Python script adds
$idx
inputs for the user to fill in later as well as text, LuaSnip uses nodes to denote inputs ands strings.
Note: This requires VimTeX to do LaTeX syntax highlighting.
Now, it’s time to address the elephant in the room: context-dependent snippets. With so many snippets and triggers, it’s important that snippets are only expanded under proper conditions - for example, math snippets should only expand in math environments, and the same goes for other specialized conditions like TikZ environments. We can take advantage of VimTeX syntax highlighting and LuaSnip conditional snippets. Here’s the functions for checking if something is in a math or specific environment:
|
|
It’s important to note how snippet engines differ in their ways of evaluating a condition - UltiSnips passes in the function call and verifies the return value, while LuaSnip only accepts the function to evaluate it on its own. So to check for environments, instead of doing something like context("env(name)")
with UltiSnips, you need to define your own helpers. However, to alleviate things, LuaSnip has condition objects, which allow you to use boolean algebra to combine different conditions.
Note: If you are also using a working completion engine that integrates with LuaSnip such as
nvim-cmp
, make sure to also set theshow_condition
parameter for tab-triggered snippets so that the completion engine does not show invalid snippets.
Here are some resources I found incredibly helpful for learning more about how LuaSnip worked.
And that’s a wrap! Hopefully this guide was helpful as an introduction to LuaSnip and a reference for moving your snippets over. If you want to check out some of my snippets, they are linked here.