Sussman Lab

LuaSnip and working with snippets

In the previous part of this guide we installed the LuaSnip plugin and connected it to our nvim-cmp completion engine. In this section I want to give a range of examples of what we can do with snippets. I’ll try to be a little bit pedagogical, and give you enough information to be able to write your own. If you want to learn more, in addition to the LuaSnip documentation and this LuaSnip for LaTeX guide, I think this video series by TJ DeVries is a great introduction. After that, be sure to check out the wiki for some cool examples that other people have come up with! But I want to emphasize: even basic snippets are extremely powerful!

So, what are snippets? They are templates for the kind of boilerplate code and markup that makes LaTeX both an extremely powerful language and a bit tedious to write. In LuaSnip a snippet is basically made up of a series of “nodes” – these nodes can be as simple as chunks of text (“text nodes”) and text that can be edited (“insert nodes”), or they can be complex objects that invoke potentially recursive functions to dynamically generate whole sequences of other nodes. Below I’ll give a few examples of the way that we can use this functionality to make writing LaTeX much (much?) faster. Each description will be preceded by a visual example showing both the actual keys I’m pressing and what happens in the editor – hopefully that makes the power of these snippets a bit more visceral.

I should mention that a few of the examples here make use of the settings in our plugin configuration – this includes some settings related to autosnippets and visual selections, and it is also where I configured all of the keys that trigger, confirm, and advance through snippets.

Text nodes

A “hello world” snippet in action. In addition to showcasing this simple snippet, this should also give a clear indication of how I’m going to be showing the keys that I’m pressing (using the Screenkey plugin) together with what happens in the buffer. Notice that I’m also using nvim-cmp with LuaSnip as a source, so instead of typing the whole snippet keyword I can just tab-complete it.

Snippet: Hello world

As a first hint of what’s to come with snippets, we’ll make the contents of our all.lua file in the luasnip/ directory the following:

return {
    s({trig="helloworld", snippetType="snippet", desc="A hellow world snippet",wordTrig=true},
        {t("Just a hint of what's to come!"),}
    ),
}

In this file we’re returning a table of snippets (s is a shorthand defined by LuaSnip for the full command require(luasnip).snippet), and in this case we are only defining one snippet. As mentioned here, the filename matters, and is a signal to LuaSnip that this snippet should be available when editing any file whatsoever. Snippets are defined by first passing in a table of information about the snippet (here, the fields trig, snippetType,desc, and wordTrig), then a table defining any nodes (here just a single text node). What do these first set of keywords do?

  • trig is the text that will trigger our snippet
  • snippetType is either “snippet” or “autosnippet”, and controls whether the snippet gets triggered automatically or only on after the keymap for ls.expand() set in your configuration.
  • desc (or dscr) is a documentation string describing the snippet (optional)
  • wordTrig is a true-by-default boolean. When true, the snippet will only trigger if there are no preceding characters. Here, i.e., typing 9helloworld would not trigger this snippet, because the word does not entirely match the trigger.

After the table of information, there will be a table of nodes. LuaSnip provides pre-build abbreviations for these nodes: t(...) are text nodes, and as you see above, they return pure text. Other things we’ll encounter include insert nodes i(...), choice nodes c(...), dynamic nodes d(...), and snippet nodes sn(...), all of which we’ll meet below.

There are, I should say, many more options and functions that LuaSnip can provide, only a portion of which we’ll cover here. Before we proceed, let’s set up space for our TeX-specific snippets. Edit the tex.lua file in the luasnip/ directory so that it is, at first, an empty table:

return {

}

In what we discuss below, every snippet should be added inside that return statement (and we’ll eventually add a few functions that we will place on the lines before the return statement).

Autosnippets

This video shows auto-triggered characters – perhaps my most used snippet. It also shows that autosnippets are not just for plain text nodes (as seen with the insert node being used for the quotation marks snippet).

Snippet: Greek letters

Although text nodes are the most basic, they are nonetheless extremely useful. Writing equations populated by Greek letters (and other symbols that require backslashes) gets a little tedious, and automatically-triggered snippets are a great solution. Here’s an example of setting up an autosnippet so that whenever I type ;a it gets immediately replaced by \alpha:

s({trig=";a", snippetType="autosnippet", desc="alpha",wordTrig=false},
    {t("\\alpha"),}
),

I like this “semi-colon character” pattern since (1) the semi-colon is on home row and (2) I otherwise would never type a semi-colon immediately followed by anything other than a space (in insert mode, at least). The only other difference with our helloworld snippet is setting wordTrig=false. This ensures that the snippet gets expanded even if there is no space before the snippet trigger (e.g., typing x_;a correctly expands to x_\alpha).

Insert nodes and snippet formatting

Insert nodes stop and wait for user input rather than just supplying a block of text (you can also pre-populate them with default text if you’d like). One can build up a snippet by interspersing text nodes and insert nodes, and create up some pretty useful functionality. The result is not particularly easy to read (and, hence, correctly code up in our tex.lua file), so before writing these insert node snippets let’s look at one of LuaSnip’s functions of convenience.

The fmta function

LuaSnip offers some convenient fmt and fmta (“format”-ing) functionality to solve this problem. The basic pattern is to replace the table of nodes with one of these format functions – under the hood the just replace their argument with a table of nodes, so they’re not doing anything other than making it easy for us to read. A simple one might look like this:

s({trig="\"", snippetType="autosnippet", desc="quotation marks"},
    fmta(
        [[``<>'' ]],
        {i(1, "text"),}
    )
),

Here the trigger is just a double-quotation mark (which had to be escaped with a slash). The content of the snippet is everything inside the double brackets, and inside the fmta function every <> pair of characters means “put a node here.” Immediately after the closing double bracket is then a list of the nodes in order. In this example we’re meeting the insert node, i. The “1” is the “jump index” of the node in question – we’re about to see an example where we can jump through a bunch of insert nodes, and we have to specify the order they should be visited – and we see that we are allowed to put placeholder/default text into the node.

Snippet: figure environments

The next sample snippet does the work of setting up a default figure environment, with four insert nodes that we can jump through and populate.

s({trig="fig", snippetType="snippet", dscr="A basic figure environment"},
    fmta(
        [[
        \begin{figure}
        \centering
        \includegraphics[width=0.9\linewidth]{<>}
        \caption{
            \textbf{<>}
            <>
            }
        \label{fig:<>}
        \end{figure}

        ]],
        { i(1,"filename"),
          i(2, "captionBold"),
          i(3, "captionText"),
          i(4,"figureLabel"),}
    )
),

Repeat nodes

A repeat node being used to simultaneously populate the argument of a matched \begin{..} and \end{...} pair of commands.

Snippet: begin/end environments

Another type of node is the “repeat” node, and this is extremely useful for setting up LaTeX’s many environments that always need to be wrapped with matching\begin and \end statements. The end of the fmta function in this example has two different insert nodes and then a repeat node, rep(1), which will literally repeat whatever (in this case) is the content of the first node in the jump list.

s({trig="env", snippetType="snippet", dscr="Begin and end an arbitrary environment"},
    fmta(
        [[
        \begin{<>}
            <>
        \end{<>}
        ]],
        {i(1),i(2),rep(1),}
    )
),

Postfixes and context-aware snippets

This might be a matter of taste, but I really enjoy the ability to use postfixes when writing. My inner monologue is saying things like “v vector dot n hat,” so it’s nice to be able to write vvec and have it turn into \vec{v}. On the other hand, this can sometime get us into trouble. Sometimes I want to be able to write a tangent unit vector that that turns into \hat{t}, but sometimes I just want to write the word “that.”

Snippet: postfix vectors and hats

Knowing that we are also going to be working with VimTeX, we can use some of its functionality to write snippets that only trigger if we are in a LaTeX “math zone” (inline math, or inside an equation-like environment). At the top of our tex.lua file, before the big table of returned snippets, let’s add the following function:

-- use vimtex to determine if we are in a math context
local function math()
    return vim.api.nvim_eval('vimtex#syntax#in_mathzone()') == 1
end 

Now we can use LuaSnip’s postfix functionality, along with it’s ability to impose conditions on snippets, as in the following:

--postfixes for vectors, hats, etc. The match pattern is '\\' plus the default (so that hats get put on greek letters,e.g.)
postfix({trig="hat", match_pattern = [[[\\%w%.%_%-%"%']+$]], snippetType="autosnippet",dscr="postfix hat when in math mode"},
    {l("\\hat{" .. l.POSTFIX_MATCH .. "}")}, 
    { condition=math }
) ,
postfix({trig="vec", match_pattern = [[[\\%w%.%_%-%"%']+$]] ,snippetType="autosnippet",dscr="postfix vec when in math mode"},
    {l("\\vec{" .. l.POSTFIX_MATCH .. "}")}, 
    { condition=math }
),

The match_pattern business is a lua pattern which says something like “if hat is typed after word which has alphanumeric characters or slashes, activate the snippet.” I added the escaped backslash (\\) to LuaSnip’s default pattern so that \alphahat (or even ;ahat, thanks to our autosnippet above) correctly gets turned into \hat{\alpha}.

Function nodes and visual selections

Snippet: Bold text

Typing \emph{} is easy enough, but sometimes you realize that you wished you had emphasized some text you already had typed out. LuaSnip provides an easy way to interact with anything that has been selected in visual mode. Here’s an example that uses a “function node” – a node which can handle user-defined functions – to take a visual selection and wrap it in a \textbf{} command.

s("textbf", 
    f(function(args, snip)
        local res, env = {}, snip.env
        for _, ele in ipairs(env.LS_SELECT_RAW) do table.insert(res, "\\textbf{" .. ele .. "}") end
        return res
    end, {})),

Kind of fun! But we’ll make this even better when we get to dynamic nodes.

Choice and snippet nodes

One can build snippets that have branching logic. To do something pedagogical and also vaguely useful I’ll introduce both a “choice” node c(...) – which allows us to cycle between some number of nodes the choice node could represent – and a “snippet” node sn(...). Snippet nodes can themselves contain tables of nodes. Above is an example of this in action, in which an autotriggered choice node lets us choose between an integral over all real numbers and one in which we specify the limits of integration. In the former case, the choice node is just a single text node; in the latter it is a snippet node that itself contains a sequence of text nodes with two insert nodes interspersed.

Snippet: Definite integrals

s({trig=";I",snippetType="autosnippet",desc="integral with infinite or inserted limits",wordTrig=false},
    fmta([[
        <>
        ]],
        {
        c(1,{
            t("\\int_{-\\infty}^\\infty"),
            sn(nil,fmta([[ \int_{<>}^{<>} ]],{i(1),i(2)})),
            })
        }
    )
),

Note that this relied on us defining an additional keymap (in my case, <ctrl-e> to cycle through the options a choice node can present. Also notice that we can use the fmta function as the thing that returns the table of nodes for the snippet node! Choice nodes combined with other node types can be used to build up interesting modular snippets that allow you to choose between related representations of the same basic data. To be honest, the only thing I actually use them for is for choosing between the LaTeX template I want to start with – this is something that happens once per document, so I like the functionality of cycling through them and don’t care about the extra keypresses. Perhaps someday I’ll think of a better use case for them.

Dynamic nodes

A “dynamic node” is like a function node, but with a crazy difference: function nodes ultimately just return text, but dynamic nodes can return snippet nodes! This allows you to do wild things with recursion, or more straightforwardly dynamically define the set of nodes you want to have a snippet expand into.

Snippet: Flexible visual selections

As a simple example, let’s modify what we did above in creating a snippet that takes a visual selection and wraps it in a \textbf{} command so that it will either do this if there is a visual selection or just give us an insert node if we’re typing in insert mode. We first add a helper function to the very top of our tex.lua file (again, before the returned table of snippets):

--test whether the parent snippet has content from a visual selection. If yes, put into a text  node, if no then start an insert node
local visualSelectionOrInsert = function(args, parent)
  if (#parent.snippet.env.LS_SELECT_RAW > 0) then
    return sn(nil, t( parent.snippet.env.LS_SELECT_RAW))
  else  -- If LS_SELECT_RAW is empty, return a blank insert node
    return sn(nil, i(1))
  end
end

This function returns a snippet node which either contains a text node with visually selected text, or an insert node if there is no such text. We can call it via a dynamic node d(...) like so:

s({trig = "emph", dscr = "the emph command, either in insert mode or wrapping a visual selection"},
    fmta("\\emph{<>}",{d(1, visualSelectionOrInsert),})
),

Snippet: Arbitrarily sized matrices

As a more complex example (which I adapted from here), we can write a snippet that creates arbitrary “NxM”-sized matrices with different delimiters. We again add a helper function at top of our tex.lua file:

local generate_matrix = function(args, snip)
	local rows = tonumber(snip.captures[2])
	local cols = tonumber(snip.captures[3])
	local nodes = {}
	local ins_indx = 1
	for j = 1, rows do
		table.insert(nodes, r(ins_indx, tostring(j) .. "x1", i(1)))
		ins_indx = ins_indx + 1
		for k = 2, cols do
			table.insert(nodes, t(" & "))
			table.insert(nodes, r(ins_indx, tostring(j) .. "x" .. tostring(k), i(1)))
			ins_indx = ins_indx + 1
		end
		table.insert(nodes, t({ "\\\\", "" }))
	end
	-- fix last node.
	nodes[#nodes] = t("\\\\")
	return sn(nil, nodes)
end

This function just returns a (potentially) long table of nodes. We can then write a snippet like the following:

s({trig = "([%sbBpvV])Mat(%d+)x(%d+)", snippetType="autosnippet", regTrig = true, wordTrig=false, dscr = "[bBpvV]matrix of A x B size"},
    fmta([[
    \begin{<>}
    <>
    \end{<>}]],
    {
    f(function(_, snip)
        if  snip.captures[1] ==" " then
            return "matrix"
        else
            return snip.captures[1] .. "matrix"
        end
    end),
    d(1, generate_matrix),
    f(function(_, snip)
        return snip.captures[1] .. "matrix"
    end)
    }),
    { condition=math }
),

The regTrig=true option tells LuaSnip to interpret the trigger as a Lua pattern: the initial ([%sbBpvV]) and (%d) groups allow us to capture symbols around “Mat” – (%d) means “any digit” and ([%sbBpvV]) means “a space or ab, B, p, v, or V character.” We could then write bMat2x2 and autoexpand a 2x2 matrix with bracket delimeters, or pMat1x15 to make a long row vector with parenthesis delimiters, etc. Wild!

Summary

I hope that after reading this we’ve accomplished two things. First, I hope to have convinced you that using snippets can substantially ease and accelerate the way you write LaTeX files. Second, I hope that this has been written in such a way that you understand the structure of these snippets enough to adapt the above and make whatever set of snippets is helpful for your LaTeX workflow.

I think it’s worth remembering that vim is extremely powerful on its own, and sometimes can do many of these tasks for us. For instance, consider the example I gave above of taking a visual selection and wrapping it in an \emph{} or \textbf{} command. With the snippet we did whatever necessary to visually highlight the desired text, and then typed <Tab>emph<Tab>. Compare that to a typical vim pattern of visually highlighting the text and typing di\emph{}<Esc><S-p> (deleting, entering insert mode, typing, exiting insert mode, and pasting). Yes, the snippet is fewer keystrokes, but at the cost of (a) remembering the snippet and (b) losing some vim muscle memory.

So, I want to close with a reminder that these snippets should be used judiciously. On the one hand: absolutely, you can use them to take care of a lot of the boilerplate code and markup that comes with the territory of writing TeX documents (or working in many programming languages). On the other hand, all of these snippets are only useful if you can actually use and remember them more quickly than typing the normal LaTeX commands. I recommend slowly building up a snippet collection that you will actually use and whose triggers make sense to you, rather than copy-pasting someone else’s collection.