__ ____
/\ \ /\ _`\ __
\ \ \ __ __ __ \ \,\L\_\ ___ /\_\ _____
\ \ \ __/\ \/\ \ /'__`\\/_\__ \ /' _ `\/\ \/\ '__`\
\ \ \L\ \ \ \_\ \/\ \L\.\_/\ \L\ \/\ \/\ \ \ \ \ \L\ \
\ \____/\ \____/\ \__/.\_\ `\____\ \_\ \_\ \_\ \ ,__/
\/___/ \/___/ \/__/\/_/\/_____/\/_/\/_/\/_/\ \ \/
\ \_\
\/_/
LuaSnip is a snippet engine written entirely in Lua. It has some great
features like inserting text (luasnip-function-node) or nodes
(luasnip-dynamic-node) based on user input, parsing LSP syntax and switching
nodes (luasnip-choice-node).
For basic setup like mappings and installing, check the README.
All code snippets in this help assume the following:
local ls = require("luasnip")
local s = ls.snippet
local sn = ls.snippet_node
local isn = ls.indent_snippet_node
local t = ls.text_node
local i = ls.insert_node
local f = ls.function_node
local c = ls.choice_node
local d = ls.dynamic_node
local r = ls.restore_node
local events = require("luasnip.util.events")
local ai = require("luasnip.nodes.absolute_indexer")
local opt = require("luasnip.nodes.optional_arg")
local extras = require("luasnip.extras")
local l = extras.lambda
local rep = extras.rep
local p = extras.partial
local m = extras.match
local n = extras.nonempty
local dl = extras.dynamic_lambda
local fmt = require("luasnip.extras.fmt").fmt
local fmta = require("luasnip.extras.fmt").fmta
local conds = require("luasnip.extras.expand_conditions")
local postfix = require("luasnip.extras.postfix").postfix
local types = require("luasnip.util.types")
local parse = require("luasnip.util.parser").parse_snippet
local ms = ls.multi_snippet
local k = require("luasnip.nodes.key_indexer").new_keyAs noted in the Loaders-Lua-section:
By default, the names from
luasnip.config.snip_envwill be used, but it's possible to customize them by settingsnip_envinsetup.
Furthermore, note that while this document assumes you have defined ls to be require("luasnip"), it is not provided in the default set of variables.
Note: the source code of snippets in GIFs is actually here, and it's slightly different from the code below.
In LuaSnip, snippets are made up of nodes. These can contain either
- static text (
textNode) - text that can be edited (
insertNode) - text that can be generated from the contents of other nodes (
functionNode) - other nodes
choiceNode: allows choosing between two nodes (which might contain more nodes)restoreNode: store and restore input to nodes
- or nodes that can be generated based on input (
dynamicNode).
Snippets are always created using the s(trigger:string, nodes:table)-function.
It is explained in more detail in Snippets, but the gist is that
it creates a snippet that contains the nodes specified in nodes, which will be
inserted into a buffer if the text before the cursor matches trigger when
ls.expand is called.
Nodes that can be jumped to (insertNode, choiceNode, dynamicNode,
restoreNode, snippetNode) all require a "jump-index" so LuaSnip knows the
order in which these nodes are supposed to be visited ("jumped to").
s("trig", {
i(1), t"text", i(2), t"text again", i(3)
})These indices don't "run" through the entire snippet, like they do in
TextMate-snippets ("$1 ${2: $3 $4}"), they restart at 1 in each nested
snippetNode:
s("trig", {
i(1), t" ", sn(2, {
t" ", i(1), t" ", i(2)
})
})(roughly equivalent to the given TextMate-snippet).
The snippets for a given filetype have to be added to LuaSnip via
ls.add_snippets(filetype, snippets). Snippets that should be accessible
globally (in all filetypes) have to be added to the special filetype all.
ls.add_snippets("all", {
s("ternary", {
-- equivalent to "${1:cond} ? ${2:then} : ${3:else}"
i(1, "cond"), t(" ? "), i(2, "then"), t(" : "), i(3, "else")
})
})It is possible to make snippets from one filetype available to another using
ls.filetype_extend, more info on that in the section API.
When a new snippet is expanded, it can be connected with the snippets that have
already been expanded in the buffer in various ways.
First of all, Luasnip distinguishes between root-snippets and child-snippets.
The latter are nested inside other snippets, so when jumping through a snippet,
one may also traverse the child-snippets expanded inside it, more or less as if
the child just contains more nodes of the parent.
Root-snippets are of course characterized by not being child-snippets.
When expanding a new snippet, it becomes a child of the snippet whose region it
is expanded inside, and a root if it is not inside any snippet's region.
If it is inside another snippet, the specific node it is inside is determined,
and the snippet then nested inside that node.
- If that node is interactive (for example, an
insertNode), the new snippet will be traversed when the node is visited, as long as the configuration-optionlink_childrenis enabled. If it is not enabled, it is possible to jump from the snippet to the node, but not the other way around. - If that node is not interactive, the snippet will be linked to the currently
active node, also such that it will not be jumped to again once it is left.
This is to prevent jumping large distances across the buffer as much as
possible. There may still be one large jump from the snippet back to the
current node it is nested inside, but that seems hard to avoid.
Thus, one should design snippets such that the regions where other snippets may be expanded are insideinsertNodes.
If the snippet is not a child, but a root, it can be linked up with the roots
immediately adjacent to it by enabling link_roots in setup.
Since by default only one root is remembered, one should also set keep_roots
if link_roots is enabled. The two are separate options, since roots that are
not linked can still be reached by ls.activate_node(). This setup (remember
roots, but don't jump to them) is useful for a super-tab like mapping (<Tab>
and jump on the same key), where one would like to still enter previous roots.
Since there would almost always be more jumps if the roots are linked, regular
<Tab> would not work almost all the time, and thus link_roots has to stay
disabled.
Every node accepts, as its last parameter, an optional table of arguments.
There are some common ones (which are listed here), and some that only apply to
some nodes (user_args for function/dynamicNode). These opts are
only mentioned if they accept options that are not common to all nodes.
Common opts:
node_ext_optsandmerge_node_ext_opts: Controlext_opts(most likely highlighting) of the node. Described in detail in ext_optskey: The node can be referred to by this key. Useful for either Key Indexer or for finding the node at runtime (See Snippets-API), for example inside adynamicNode. The keys do not have to be unique across the entire lifetime of the snippet, but at any point in time, the snippet may contain each key only once. This means it is fine to return a keyed node from adynamicNode, because even if it will be generated multiple times, those will not be valid at the same time.node_callbacks: Define event-callbacks for this node (see events).
Accepts a table that maps an event, e.g.events.enterto the callback (essentially the same ascallbackspassed tos, only that there is no first mapping from jump-index to the table of callbacks).
get_jump_index(): this method returns the jump-index of a node. If a node doesn't have a jump-index, this method returnsnilinstead.get_buf_position(opts) -> {from_position, to_position}: Determines the range of the buffer occupied by this node.from- andto_positionarerow,column-tuples,0,0-indexed (first line is 0, first column is 0) and end-inclusive (see:h api-indexing, this is extmarks indexing).opts:table|nil, options, valid keys are:raw:bool, defaulttrue. This can be used to switch between byte-columns (raw=true) and visual columns (raw=false). This makes a difference if the line contains characters represented by multiple bytes in UTF, for exampleÿ.
The most direct way to define snippets is s:
s({trig="trigger"}, {})(This snippet is useless beyond serving as a minimal example)
s(context, nodes, opts) -> snippet
-
context: Either table or a string. Passing a string is equivalent to passing{ trig = context }The following keys are valid:
-
trig: string, the trigger of the snippet. If the text in front of (to the left of) the cursor whenls.expand()is called matches it, the snippet will be expanded.
By default, "matches" means the text in front of the cursor matches the trigger exactly, this behavior can be modified throughtrigEngine -
name: string, can be used by e.g.nvim-competo identify the snippet. -
desc(ordscr): string, description of the snippet, \n-separated or table for multiple lines. -
wordTrig: boolean, if true, the snippet is only expanded if the word ([%w_]+) before the cursor matches the trigger entirely. True by default. -
regTrig: boolean, whether the trigger should be interpreted as a Lua pattern. False by default.
Consider settingtrigEngineto"pattern"instead, it is more expressive, and in line with other settings. -
trigEngine: (function|string), determines howtrigis interpreted, and what it means for it to "match" the text in front of the cursor.
This behavior can be completely customized by passing a function, but the predefined ones, which are accessible by passing their identifier, should suffice in most cases:"plain": the default-behavior, the trigger has to match the text before the cursor exactly."pattern": the trigger is interpreted as a Lua pattern, and is a match iftrig .. "$"matches the line up to the cursor. Capture-groups will be accessible assnippet.captures."ecma": the trigger is interpreted as an ECMAscript-regex, and is a match iftrig .. "$"matches the line up to the cursor. Capture-groups will be accessible assnippet.captures.
ThistrigEnginerequiresjsregexp(see LSP-snippets-transformations) to be installed, if it is not, this engine will behave like"plain"."vim": the trigger is interpreted as a vim-regex, and is a match iftrig .. "$"matches the line up to the cursor. As with the other regex/pattern-engines, captures will be available assnippet.captures, but there is one caveat: the matching is done usingmatchlist, so for now empty-string submatches will be interpreted as unmatched, and the correspondingsnippet.capture[i]will benil(this will most likely change, don't rely on this behavior).
Besides these predefined engines, it is also possible to create new ones: Instead of a string, pass a function which satisfies
trigEngine(trigger, opts) -> (matcher(line_to_cursor, trigger) -> whole_match, captures)(i.e. the function receivestrigandtrigEngineOptscan, for example, precompile a regex, and then returns a function responsible for determining whether the current cursor-position (represented by the line up to the cursor) matches the trigger (it is passed again here so engines which don't do any trigger-specific work (like compilation) can just return a staticmatcher), and what the capture-groups are).
Thelua-engine, for example, can be implemented like this:local function matcher(line_to_cursor, trigger) -- look for match which ends at the cursor. -- put all results into a list, there might be many capture-groups. local find_res = { line_to_cursor:find(trigger .. "$") } if #find_res > 0 then -- if there is a match, determine matching string, and the -- capture-groups. local captures = {} -- find_res[1] is `from`, find_res[2] is `to` (which we already know -- anyway). local from = find_res[1] local match = line_to_cursor:sub(from, #line_to_cursor) -- collect capture-groups. for i = 3, #find_res do captures[i - 2] = find_res[i] end return match, captures else return nil end end local function engine(trigger) -- don't do any special work here, can't precompile lua-pattern. return matcher end
The predefined engines are defined in
trig_engines.lua, read it for more examples. -
trigEngineOpts:table<string, any>, options for the usedtrigEngine.
The valid options are:max_len: number, upper bound on the length of the trigger.
If this is set, theline_to_cursorwill be truncated (from the cursor of course) tomax_lencharacters before performing the match.
This is implemented because feeding longline_to_cursorinto e.g. the pattern-trigEnginewill hurt performance quite a bit (see issue Luasnip#1103).
This option is implemented for alltrigEngines.
-
docstring: string, textual representation of the snippet, specified likedesc. Overrides docstrings loaded fromjson. -
docTrig: string, used asline_to_cursorduring docstring-generation. This might be relevant if the snippet relies on specific values in the capture-groups (for example, numbers, which won't work with the default$CAPTURESNused during docstring-generation) -
hidden: boolean, hint for completion-engines. If set, the snippet should not show up when querying snippets. -
priority: positive number, Priority of the snippet, 1000 by default.
Snippets with high priority will be matched to a trigger before those with a lower one. The priority for multiple snippets can also be set inadd_snippets. -
snippetType: string, should be eithersnippetorautosnippet(ATTENTION: singular form is used), decides whether this snippet has to be triggered byls.expand()or whether is triggered automatically (don't forget to setls.config.setup({ enable_autosnippets = true })if you want to use this feature). If unset it depends on how the snippet is added of which type the snippet will be. -
resolveExpandParams:fn(snippet, line_to_cursor, matched_trigger, captures) -> table|nil, wheresnippet:Snippet, the expanding snippet objectline_to_cursor:string, the line up to the cursor.matched_trigger:string, the fully matched trigger (can be retrieved fromline_to_cursor, but we already have that info here :D)captures:capturesas returned bytrigEngine.
This function will be evaluated in
Snippet:matches()to decide whether the snippet can be expanded or not.
Returns a table if the snippet can be expanded,nilif can not. The returned table can contain any of these fields:trigger:string, the fully matched trigger.captures:table, this list could update the capture-groups from parameter in snippet expansion. Bothtriggerandcapturescan override the values returned viatrigEngine.clear_region:{ "from": {<row>, <column>}, "to": {<row>, <column>} }, both (0, 0)-indexed, the region where text has to be cleared before inserting the snippet.env_override:map string->(string[]|string), override or extend the snippet's environment (snip.env)
If any of these is
nil, the default is used (triggerandcapturesas returned bytrigEngine,clear_regionsuch that exactly the trigger is deleted, no overridden environment-variables).A good example for the usage of
resolveExpandParamscan be found in the implementation ofpostfix. -
condition:fn(line_to_cursor, matched_trigger, captures) -> bool, whereline_to_cursor:string, the line up to the cursor.matched_trigger:string, the fully matched trigger (can be retrieved fromline_to_cursor, but we already have that info here :D)captures: if the trigger is pattern, this list contains the capture-groups. Again, could be computed fromline_to_cursor, but we already did so.
This function can prevent manual snippet expansion via
ls.expand().
Returntrueto allow expansion, andfalseto prevent it. -
show_condition:f(line_to_cursor) -> bool.line_to_cursor:string, the line up to the cursor.
This function is (should be) evaluated by completion engines, indicating whether the snippet should be included in current completion candidates.
Defaults to a function returningtrue.
This is different fromconditionbecauseconditionis evaluated by LuaSnip on snippet expansion (and thus has access to the matched trigger and captures), whileshow_conditionis (should be) evaluated by the completion engines when scanning for available snippet candidates. -
filetype:string, the filetype of the snippet. This overrides the filetype the snippet is added (viaadd_snippet) as.
-
-
nodes: A single node or a list of nodes. The nodes that make up the snippet. -
opts: A table with the following valid keys:callbacks: Contains functions that are called upon entering/leaving a node of this snippet.
For example: to print text upon entering the second node of a snippet,callbacksshould be set as follows:To register a callback for the snippets' own events, the key{ -- position of the node, not the jump-index!! -- s("trig", {t"first node", t"second node", i(1, "third node")}). [2] = { [events.enter] = function(node, _event_args) print("2!") end } }[-1]may be used. More info on events in eventschild_ext_opts,merge_child_ext_opts: Controlext_optsapplied to the children of this snippet. More info on those in the ext_opts-section.
The opts-table, as described here, can also be passed to e.g. snippetNode
and indentSnippetNode.
It is also possible to set condition and show_condition (described in the
documentation of the context-table) from opts. They should, however, not be
set from both.
Snippets contain some interesting tables during runtime:
snippet.env: Contains variables used in the LSP-protocol, for exampleTM_CURRENT_LINEorTM_FILENAME. It's possible to add customized variables here too, check Variables-Environment Namespacessnippet.captures: If the snippet was triggered by a pattern (regTrig), and the pattern contained capture-groups, they can be retrieved here.snippet.trigger: The string that triggered this snippet. Again, only interesting if the snippet was triggered throughregTrig, for getting the full match.
These variables/tables primarily come in handy in dynamic/functionNodes, where
the snippet can be accessed through the immediate parent (parent.snippet),
which is passed to the function.
(in most cases parent == parent.snippet, but the parent of the dynamicNode
is not always the surrounding snippet, it could be a snippetNode).
invalidate(): call this method to effectively remove the snippet. The snippet will no longer be able to expand viaexpandorexpand_auto. It will also be hidden from lists (at least if the plugin creating the list respects thehidden-key), but it might be necessary to callls.refresh_notify(ft)after invalidating snippets.get_keyed_node(key): Returns the currently visible node associated withkey.
The most simple kind of node; just text.
s("trigger", { t("Wow! Text!") })This snippet expands to
Wow! Text!⎵
where ⎵ is the cursor.
Multiline strings can be defined by passing a table of lines rather than a string:
s("trigger", {
t({"Wow! Text!", "And another line."})
})t(text, node_opts):
text:stringorstring[]node_opts:table, see Node
These Nodes contain editable text and can be jumped to- and from (e.g.
traditional placeholders and tabstops, like $1 in TextMate-snippets).
The functionality is best demonstrated with an example:
s("trigger", {
t({"After expanding, the cursor is here ->"}), i(1),
t({"", "After jumping forward once, cursor is here ->"}), i(2),
t({"", "After jumping once more, the snippet is exited there ->"}), i(0),
})The Insert Nodes are visited in order 1,2,3,..,n,0.
(The jump-index 0 also has to belong to an insertNode!)
So the order of InsertNode-jumps is as follows:
- After expansion, the cursor is at InsertNode 1,
- after jumping forward once at InsertNode 2,
- and after jumping forward again at InsertNode 0.
If no 0-th InsertNode is found in a snippet, one is automatically inserted after all other nodes.
The jump-order doesn't have to follow the "textual" order of the nodes:
s("trigger", {
t({"After jumping forward once, cursor is here ->"}), i(2),
t({"", "After expanding, the cursor is here ->"}), i(1),
t({"", "After jumping once more, the snippet is exited there ->"}), i(0),
})The above snippet will behave as follows:
- After expansion, we will be at InsertNode 1.
- After jumping forward, we will be at InsertNode 2.
- After jumping forward again, we will be at InsertNode 0.
An important (because here Luasnip differs from other snippet engines) detail is that the jump-indices restart at 1 in nested snippets:
s("trigger", {
i(1, "First jump"),
t(" :: "),
sn(2, {
i(1, "Second jump"),
t" : ",
i(2, "Third jump")
})
})as opposed to e.g. the TextMate syntax, where tabstops are snippet-global:
${1:First jump} :: ${2: ${3:Third jump} : ${4:Fourth jump}}(this is not exactly the same snippet of course, but as close as possible) (the restart-rule only applies when defining snippets in Lua, the above TextMate-snippet will expand correctly when parsed).
i(jump_index, text, node_opts)
jump_index:number, this determines when this node will be jumped to (see Basics-Jump-Index).text:string|string[], a single string for just one line, a list with >1 entries for multiple lines. This text will beSELECTed when theinsertNodeis jumped into.node_opts:table, described in Node
If the jump_index is 0, replacing its' text will leave it outside the
insertNode (for reasons, check out Luasnip#110).
Function Nodes insert text based on the content of other nodes using a user-defined function:
local function fn(
args, -- text from i(2) in this example i.e. { { "456" } }
parent, -- parent snippet or parent node
user_args -- user_args from opts.user_args
)
return '[' .. args[1][1] .. user_args .. ']'
end
s("trig", {
i(1), t '<-i(1) ',
f(fn, -- callback (args, parent, user_args) -> string
{2}, -- node indice(s) whose text is passed to fn, i.e. i(2)
{ user_args = { "user_args_value" }} -- opts
),
t ' i(2)->', i(2), t '<-i(2) i(0)->', i(0)
})f(fn, argnode_references, node_opts):
-
fn:function(argnode_text, parent, user_args1,...,user_argsn) -> text-
argnode_text:string[][], the text currently contained in the argnodes (e.g.{{line1}, {line1, line2}}). The snippet indent will be removed from all lines following the first. -
parent: The immediate parent of thefunctionNode.
It is included here as it allows easy access to some information that could be useful in functionNodes (see Snippets-Data for some examples).
Many snippets access the surrounding snippet just asparent, but if thefunctionNodeis nested within asnippetNode, the immediate parent is asnippetNode, not the surrounding snippet (only the surrounding snippet contains data likeenvorcaptures). -
user_args: Theuser_argspassed inopts. Note that there may be multipleuser_args(e.g.user_args1, ..., user_argsn).
fnshall return a string, which will be inserted as is, or a table of strings for multiline strings, where all lines following the first will be prefixed with the snippets' indentation. -
-
argnode_references:node_reference[]|node_refernce|nil.
Either no, a single, or multiple Node References. Changing any of these will trigger a re-evaluation offn, and insertion of the updated text.
If no node reference is passed, thefunctionNodeis evaluated once upon expansion. -
node_opts:table, see Node. One additional key is supported:-
user_args:any[], these will be passed tofnasuser_arg1-user_argn. These make it easier to reuse similar functions, for example a functionNode that wraps some text in different delimiters ((),[], ...).local function reused_func(_,_, user_arg1) return user_arg1 end s("trig", { f(reused_func, {}, { user_args = {"text"} }), f(reused_func, {}, { user_args = {"different text"} }), })
-
Examples:
-
Use captures from the regex trigger using a functionNode:
s({trig = "b(%d)", regTrig = true}, f(function(args, snip) return "Captured Text: " .. snip.captures[1] .. "." end, {}) )
-
argnodes_textduring function evaluation:s("trig", { i(1, "text_of_first"), i(2, {"first_line_of_second", "second_line_of_second"}), f(function(args, snip) --here -- order is 2,1, not 1,2!! end, {2, 1} )})
At
--here,argswould look as follows (provided no text was changed after expansion):args = { {"first_line_of_second", "second_line_of_second"}, {"text_of_first"} }
-
s("trig", { i(1, "text_of_first"), i(2, {"first_line_of_second", "second_line_of_second"}), f(function(args, snip) -- just concat first lines of both. return args[1][1] .. args[2][1] end, {ai[2], ai[1]} )})
If the function only performs simple operations on text, consider using
the lambda from luasnip.extras (See Extras-Lambda)
Node references are used to refer to other nodes in various parts of LuaSnip's
API.
For example, argnodes in functionNode, dynamicNode or lambda are
node references.
These references can be either of:
number: the jump-index of the node. This will be resolved relative to the parent of the node this is passed to. (So, only nodes with the same parent can be referenced. This is very easy to grasp, but also limiting)key_indexer: the key of the node, if it is present. This will come in handy if the node that is being referred to is not in the same snippet/snippetNode as the one the node reference is passed to. Also, it is the proper way to refer to a non-interactive node (a functionNode, for example)absolute_indexer: the absolute position of the node. Just likekey_indexer, it allows addressing non-sibling nodes, but is a bit more awkward to handle since a path from root to node has to be determined, whereaskey_indexerjust needs the key to match.
Due to this,key_indexershould be generally preferred. (More information in Absolute Indexer).node: just the node. Usage of this is discouraged since it can lead to subtle errors (for example, if the node passed here is captured in a closure and therefore not copied with the remaining tables in the snippet; there's a big comment about just this in commit8bfbd61).
ChoiceNodes allow choosing between multiple nodes.
s("trig", c(1, {
t("Ugh boring, a text node"),
i(nil, "At least I can edit something now..."),
f(function(args) return "Still only counts as text!!" end, {})
}))c(pos, choices, opts?): LuaSnip.ChoiceNode: Create a new choiceNode from a list of choices. The
first item in this list is the initial choice, and it can be changed while any node of a choice is
active. So, if all choices should be reachable, every choice has to have a place for the cursor to
stop at.
If the choice is a snippetNode like sn(nil, {...nodes...}) the given nodes have to contain an
insertNode (e.g. i(1)). Using an insertNode or textNode directly as a choice is also fine,
the latter is special-cased to have a jump-point at the beginning of its text.
-
pos: integerJump-index of the node. (See Basics-Jump-Index) -
choices: (LuaSnip.Node|LuaSnip.Node[])[]A list of nodes that can be switched between interactively. If a list of nodes is passed as a choice, it will be turned into a snippetNode. Jumpable nodes that generally need a jump-index don't need one when used as a choice since they inherit the choiceNode's jump-index anyway. -
opts?: LuaSnip.Opts.ChoiceNode?Additional optional arguments.
Valid keys are:-
restore_cursor?: boolean?If set, the currently active node is looked up in the switched-to choice, and the cursor restored to preserve the current position relative to that node. The node may be found if arestoreNodeis present in both choice. Defaults tofalse, as enabling might lead to decreased performance.It's possible to override the default by wrapping the
choiceNodeconstructor in another function that setsopts.restore_cursortotrueand then using that to constructchoiceNodes:local function restore_cursor_choice(pos, choices, opts) opts = opts or {} opts.restore_cursor = true return c(pos, choices, opts) end
Consider passing this override into
snip_env. -
node_callbacks?: { [("change_choice"|"enter"...)]: fun(...) -> ... }? -
node_ext_opts?: LuaSnip.NodeExtOpts?Pass these opts through to the underlying extmarks representing the node. Notably, this enables highlighting the nodes, and allows the highlight to be different based on the state of the node/snippet. See ext_opts -
merge_node_ext_opts?: boolean?Whether to use the parents'ext_optsto compute this nodes'ext_opts. -
key: anySome unique value (strings seem useful) to identify this node. This is useful for Key Indexer or for finding the node at runtime (See Snippets-API These keys don't have to be unique across the entire lifetime of the snippet, but every key should occur only once at the same time. This means it is fine to return a keyed node from a dynamicNode, because even if it will be generated multiple times, the same key not occur twice at the same time.
-
Examples:
c(1, {
t"some text", -- textNodes are just stopped at.
i(nil, "some text"), -- likewise.
sn(nil, {t"some text"}) -- this will not work!
sn(nil, {i(1), t"some text"}) -- this will.
})The active choice for a choiceNode can be changed by either calling one of
ls.change_choice(1) (forwards) or ls.change_choice(-1) (backwards), or by
calling ls.set_choice(choice_indx).
One way to easily interact with choiceNodes is binding change_choice(1/-1) to
keys:
-- set keybinds for both INSERT and VISUAL.
vim.api.nvim_set_keymap("i", "<C-n>", "<Plug>luasnip-next-choice", {})
vim.api.nvim_set_keymap("s", "<C-n>", "<Plug>luasnip-next-choice", {})
vim.api.nvim_set_keymap("i", "<C-p>", "<Plug>luasnip-prev-choice", {})
vim.api.nvim_set_keymap("s", "<C-p>", "<Plug>luasnip-prev-choice", {})Apart from this, there is also a picker (see select_choice
where no cycling is necessary and any choice can be selected right away, via
vim.ui.select.
SnippetNodes directly insert their contents into the surrounding snippet.
This is useful for choiceNodes, which only accept one child, or
dynamicNodes, where nodes are created at runtime and inserted as a
snippetNode.
Their syntax is similar to s, however, where snippets require a table
specifying when to expand, snippetNodes, similar to insertNodes, expect
a jump-index.
s("trig", sn(1, {
t("basically just text "),
i(1, "And an insertNode.")
}))sn(jump_index, nodes, node_opts)
-
jump_index:number, the usual Jump-Index. -
nodes:node[]|node, just like fors.
Note thatsnippetNodes don't accept ani(0), so the jump-indices of the nodes inside them have to be in1,2,...,n. -
node_opts:table: again, the keys common to all nodes (documented in Node) are supported, but alsocallbacks,child_ext_optsandmerge_child_ext_opts,
which are further explained in Snippets.
By default, all nodes are indented at least as deep as the trigger. With these nodes it's possible to override that behavior:
s("isn", {
isn(1, {
t({"This is indented as deep as the trigger",
"and this is at the beginning of the next line"})
}, "")
})(Note the empty string passed to isn).
Indent is only applied after line breaks, so it's not possible to remove indent
on the line where the snippet was triggered using ISN (That is possible via
regex triggers where the entire line before the trigger is matched).
Another nice use case for ISN is inserting text, e.g. // or some other comment
string before the nodes of the snippet:
s("isn2", {
isn(1, t({"//This is", "A multiline", "comment"}), "$PARENT_INDENT//")
})Here the // before This is is important, once again, because indent is only
applied after line breaks.
To enable such usage, $PARENT_INDENT in the indentstring is replaced by the
parent's indent.
isn(jump_index, nodes, indentstring, node_opts)
All of these parameters except indentstring are exactly the same as in
SnippetNode.
indentstring:string, will be used to indent the nodes inside thissnippetNode.
All occurrences of"$PARENT_INDENT"are replaced with the actual indent of the parent.
Very similar to functionNode, but returns a snippetNode instead of just text, which makes them very powerful as parts of the snippet can be changed based on user input.
d(jump_index, function, node-references, opts):
-
jump_index:number, just like all jumpable nodes, its' position in the jump-list (Basics-Jump-Index). -
function:fn(args, parent, old_state, user_args) -> snippetNodeThis function is called when the argnodes' text changes. It should generate and return (wrapped inside asnippetNode) nodes, which will be inserted at the dynamicNode's place.
args,parentanduser_argsare also explained in FunctionNodeargs:table of text(snippetstring_args == false) orsnippetString(otherwise). Represents content of the nodes this dynamicNode depends on. Their index in thenode_references-list determines the position in this list.parent: the immediate parent of thedynamicNode.old_state: a user-defined table. This table may contain anything; its intended usage is to preserve information from the previously generatedsnippetNode. If thedynamicNodedepends on other nodes, it may be reconstructed, which means all user input (text inserted ininsertNodes, changed choices) to the previousdynamicNodeis lost.
Theold_statetable must be stored insnippetNodereturned by the function (snippetNode.old_state).
The second example below illustrates the usage ofold_state.user_args: passed through fromdynamicNode-opts; may have more than one argument.
-
node_references:node_reference[]|node_references|nil, Node References to the nodes the dynamicNode depends on: if any of these trigger an update (for example, if the text inside them changes), thedynamicNodes' function will be executed, and the result inserted at thedynamicNodes place.
(dynamicNodebehaves exactly the same asfunctionNodein this regard). -
opts: In addition to the common Node-keys, there is:user_args, which is described in FunctionNode.snippetstring_args, boolean: If set,functionreceives a list of Snippetstring instead ofstring[].falseby default.
Examples:
This dynamicNode inserts an insertNode which copies the text inside the
first insertNode.
s("trig", {
t"text: ", i(1), t{"", "copy: "},
d(2, function(args)
-- the returned snippetNode doesn't need a position; it's inserted
-- "inside" the dynamicNode.
return sn(nil, {
-- jump-indices are local to each snippetNode, so restart at 1.
i(1, args[1])
})
end,
{1})
})This snippet makes use of old_state to count the number of updates.
To store/restore values generated by the dynamicNode or entered into
insert/choiceNode, consider using the shortly-introduced restoreNode instead
of old_state.
local function count(_, _, old_state)
old_state = old_state or {
updates = 0
}
old_state.updates = old_state.updates + 1
local snip = sn(nil, {
t(tostring(old_state.updates))
})
snip.old_state = old_state
return snip
end
ls.add_snippets("all",
s("trig", {
i(1, "change to update"),
d(2, count, {1})
})
)As with functionNode, user_args can be used to reuse similar dynamicNode-
functions.
While the previous examples only showed dynamicNodes that depend on nodes outside of itself, it's possible to update a dynamicNode in response to a change to a node within it:
ls.snip_expand(s("trig", {
d(1, function(args)
if not args[1] then
-- the arg does not exist -> provide a default.
return sn(nil, {i(1, "asdf", {key = "ins"})})
else
-- This branch is only take after the dynamicNode was updated once.
-- Now we can perform the actual "task" of this dynamicNode:
-- replacing all occurences of "a" with "e".
return sn(nil, {i(1, args[1]:gsub("a", "e"), {key = "ins"})})
end
end, {opt(k("ins"))}, { snippetstring_args = true })
}))The example above shows a dynamicNode that will substitute all occurrences of "a" within it with an "e". Important for this to work are:
- Use an Optional Noderef
opt(k("ins"))to reference the generated insertNode that will eventually update the dynamicNode on changes: A dynamicNode does not update when one of its argnodes is missing, so in order to get anything out of the node, we needopt. - Set
snippetstring_args: This ensures that snippets expanded inside the dynamicNode are preserved during the update! While not completely necessary, this is a good idea because it can be annoying to lose jump-points only because the snippet they belong is inside such a self-dependent dynamicNode.
When using these self-dependent dynamicNodes it is a really good idea to give nodes that can cause an update some unique key, or make sure that they are inside of a restoreNode. If this is not done, LuaSnip may not find a node that is equivalent to the one that contained the cursor before the update, and thus may have to jump into the dynamicNode anew, which may be unexpected.
Another danger is in accidentally constructing an infinite loop of updates. Right now, LuaSnip is very indiscriminate in updating dynamicNodes, and if given a snippet like
s("srep", {
d(1, function(args)
if not args[1] then
return sn(nil, {i(1, "sdf", {key = "ins"})})
else
return sn(nil, {i(1, args[1]:gsub("a", "aa"), {key = "ins"})})
end
end, {opt(k("ins"))}, {snippetstring_args = true})
}),any "a" is replaced with "aa", ad infinitum (or until the process is killed, which is likely soon).
These infinite loops may also be caused by nested self-dependent dynamicNodes,
think one that replaces "a" with "e", and another that changes "e" back to "a",
so it's best to build in some kind of safeguard into the generating function.
One could for example skip the update once the argnode exceeds a certain length.
This node can store and restore a snippetNode as is. This includes changed choices and changed text. Its' usage is best demonstrated by an example:
s("paren_change", {
c(1, {
sn(nil, { t("("), r(1, "user_text"), t(")") }),
sn(nil, { t("["), r(1, "user_text"), t("]") }),
sn(nil, { t("{"), r(1, "user_text"), t("}") }),
}),
}, {
stored = {
-- key passed to restoreNodes.
["user_text"] = i(1, "default_text")
}
})Here the text entered into user_text is preserved upon changing choice.
r(jump_index, key, nodes, node_opts):
jump_index, when to jump to this node.key,string:restoreNodes with the same key share their content.nodes,node[]|node: the content of therestoreNode.
Can either be a single node, or a table of nodes (both of which will be wrapped inside asnippetNode, except if the single node already is asnippetNode).
The content for a given key may be defined multiple times, but if the contents differ, it's undefined which will actually be used.
If a key's content is defined in adynamicNode, it will not be initially used forrestoreNodesoutside thatdynamicNode. A way around this limitation is defining the content in therestoreNodeoutside thedynamicNode.
The content for a key may also be defined in the opts-parameter of the
snippet-constructor, as seen in the example above. The stored-table accepts
the same values as the nodes-parameter passed to r.
If no content is defined for a key, it defaults to the empty insertNode.
An important-to-know limitation of restoreNode is that, for a given key, only
one may be visible at a time. See
this issue for details.
The restoreNode is especially useful for storing input across updates of a
dynamicNode. Consider this:
local function simple_restore(args, _)
return sn(nil, {i(1, args[1]), i(2, "user_text")})
end
s("rest", {
i(1, "preset"), t{"",""},
d(2, simple_restore, 1)
})Every time the i(1) in the outer snippet is changed, the text inside the
dynamicNode is reset to "user_text". This can be prevented by using a
restoreNode:
local function simple_restore(args, _)
return sn(nil, {i(1, args[1]), r(2, "dyn", i(nil, "user_text"))})
end
s("rest", {
i(1, "preset"), t{"",""},
d(2, simple_restore, 1)
})Now the entered text is stored.
restoreNodes indent is not influenced by indentSnippetNodes right now. If
that really bothers you feel free to open an issue.
Snippetstrings can store the content of an insertNode as-is. This includes both the regular text, and expanded snippets. The primary purpose of Snippetstrings is to facilitate easy modifications of text in dynamicNode, while preserving snippets and cursor-positions.
A Snippetstring supports the string-functions lower, upper, gsub and
sub, and the ..-metamethod. While lower, upper, and .. will always
preserve snippets inside the Snippetstring, sub will replace partially
contained snippets with their text, while gsub will do so if a replacement
crosses a node-boundary.
A very flexible way of referencing nodes (Node Reference).
While the straightforward way of addressing nodes via their
Jump-Index suffices in most cases, a dynamic/functionNode can
only depend on nodes in the same snippet(Node), its siblings (since the index is
interpreted as relative to their parent). Accessing a node with a different
parent is thus not possible. Secondly, and less relevant, only nodes that
actually have a jump-index can be referred to (a functionNode, for example,
cannot be depended on).
Both of these restrictions are lifted with key_indexer:
It allows addressing nodes by their key, which can be set when the node is
constructed, and is wholly independent of the nodes' position in the snippet,
thus enabling descriptive labeling.
The following snippets demonstrate the issue and the solution by using
key_indexer:
First, the addressed problem of referring to nodes outside the functionNodes
parent:
s("trig", {
i(1), c(2, {
sn(nil, {
t"cannot access the argnode :(",
f(function(args)
return args[1]
end, {???}) -- can't refer to i(1), since it isn't a sibling of `f`.
}),
t"sample_text"
})
})And the solution: first give the node we want to refer to a key, and then pass
the same to the functionNode.
s("trig", {
i(1, "", {key = "i1-key"}), c(2, {
sn(nil, { i(1),
t"can access the argnode :)",
f(function(args)
return args[1]
end, k("i1-key") )
}),
t"sample_text"
})
})opt wraps another node-reference and makes the wrapped node not strictly
required for performing an update. This means, amongst other things, that a
dynamicNodes can be updated by nodes within it:
ls.snip_expand(s("trig", {
d(1, function(args)
if not args[1] then
-- the arg does not exist -> provide a default.
return sn(nil, {i(1, "asdf", {key = "ins"})})
else
-- This branch is only take after the dynamicNode was updated once.
-- Now we can perform the actual "task" of this dynamicNode:
-- replacing all occurences of "a" with "e".
return sn(nil, {i(1, args[1][1]:gsub("a", "e"), {key = "ins"})})
end
end, {opt(k("ins"))})
}))absolute_indexer allows accessing nodes by their unique jump-index path from
the snippet-root. This makes it almost as powerful as Key Indexer,
but again removes the possibility of referring to non-jumpable nodes and makes
it all a bit more error-prone since the jump-index paths are hard to follow, and
(unfortunately) have to be a bit verbose (see the long example of
absolute_indexer-positions below). Consider just using Key Indexer
instead.
(The solution-snippet from Key Indexer, but using ai instead.)
s("trig", {
i(1), c(2, {
sn(nil, { i(1),
t"can access the argnode :)",
f(function(args)
return args[1]
end, ai(1) )
}),
t"sample_text"
})
})There are some quirks in addressing nodes:
s("trig", {
i(2), -- ai[2]: indices based on jump-index, not position.
sn(1, { -- ai[1]
i(1), -- ai[1][1]
t"lel", -- not addressable.
i(2) -- ai[1][2]
}),
c(3, { -- ai[3]
i(nil), -- ai[3][1]
t"lel", -- ai[3][2]: choices are always addressable.
}),
d(4, function() -- ai[4]
return sn(nil, { -- ai[4][0]
i(1), -- ai[4][0][1]
})
end, {}),
r(5, "restore_key", -- ai[5]
i(1) -- ai[5][0][1]: restoreNodes always store snippetNodes.
),
r(6, "restore_key_2", -- ai[6]
sn(nil, { -- ai[6][0]
i(1) -- ai[6][0][1]
})
)
})Note specifically that the index of a dynamicNode differs from that of the
generated snippetNode, and that restoreNodes (internally) always store a
snippetNode, so even if the restoreNode only contains one node, that node has
to be accessed as ai[restoreNodeIndx][0][1].
absolute_indexers' can be constructed in different ways:
ai[1][2][3]ai(1, 2, 3)ai{1, 2, 3}
are all the same node.
There are situations where it might be comfortable to access a snippet in
different ways. For example, one might want to enable auto-triggering in regions
where the snippets usage is common, while leaving it manual-only in others.
This is where ms should be used: A single snippet can be associated with multiple
contexts (the context-table determines the conditions under which a snippet
may be triggered).
This has the advantage (compared with just registering copies) that all
contexts are backed by a single snippet, and not multiple, and it's (at least
should be :D) more comfortable to use.
ms(contexts, nodes, opts) -> addable:
contexts: table containing list ofcontexts, and some keywords.
contextare described in Snippets, here they may also be tables or strings.
So far, there is only one valid keyword:common: Accepts yet another context.
The options incommonare applied to (but don't override) the other contexts specified incontexts.
nodes: List of nodes, exactly like in Snippets.opts: Table, options for this function:common_opts: The snippet-options (see also Snippets) applied to the snippet generated fromnodes.
The returned object is an addable, something which can be passed to
add_snippets, or returned from the lua-loader.
Examples:
ls.add_snippets("all", {
ms({"a", "b"}, {t"a or b"})
})ls.add_snippets("all", {
ms({
common = {snippetType = "autosnippet"},
"a",
"b"
}, {
t"a or b (but autotriggered!!)"
})
})ls.add_snippets("all", {
ms({
common = {snippetType = "autosnippet"},
{trig = "a", snippetType = "snippet"},
"b",
{trig = "c", condition = function(line_to_cursor)
return line_to_cursor == ""
end}
}, {
t"a or b (but autotriggered!!)"
})
})This example shows how to access the snippet opts for MultiSnippets.
Note that unlike a s snippet these opts must be in the common_opts
table item.
ls.addsnippets('all', {
ms({ 'test' }, { t('Access multisnippet opts') }, {
common_opts = {
callbacks = {
[-1] = {
[events.pre_expand] = function()
vim.notify('multisnippet opts callback')
end,
},
},
},
}),
}),A shortcut for functionNodes that only do very basic string
manipulation.
l(lambda, argnodes):
lambda: An object created by applying string-operations tol._n, objects representing thenth argnode.
For example:l._1:gsub("a", "e")replaces all occurrences of "a" in the text of the first argnode with "e", orl._1 .. l._2concatenates text of the first and second argnode. If an argnode contains multiple lines of text, they are concatenated with"\n"prior to any operation.
argnodes, a Node Reference, just like in function- and dynamicNode.
There are many examples for lambda in Examples/snippets.lua
match can insert text based on a predicate (again, a shorthand for functionNode).
match(argnodes, condition, then, else):
-
argnode: A single Node Reference. May not be nil, or a table. -
conditionmay be either ofstring: interpreted as a Lua pattern. Matched on the\n-joined (in case it's multiline) text of the first argnode (args[1]:match(condition)).function:fn(args, snip) -> bool: takes the same parameters as thefunctionNode-function, any value other than nil or false is interpreted as a match.lambda:l._nis the\n-joined text of the nth argnode.
Useful if string manipulations have to be performed before the string is matched.
Should end withmatch, but any other truthy result will be interpreted as matching.
-
thenis inserted if the condition matches, -
elseif it does not.
Both then and else can be either text, lambda or function (with the same parameters as
specified above).
then's default-value depends on the condition:
pattern: Simply the return value from thematch, e.g. the entire match, or, if there were capture groups, the first capture group.function: the return value of the function if it is either a string, or a table (if there is nothen, the function cannot return a table containing something other than strings).lambda: Simply the first value returned by the lambda.
Examples:
-
match(n, "^ABC$", "A")s("extras1", { i(1), t { "", "" }, m(1, "^ABC$", "A") })
Inserts "A" if the node with jump-index
nmatches "ABC" exactly, nothing otherwise. -
match(n, lambda._1:match(lambda._1:reverse()), "PALINDROME")s("extras2", { i(1, "INPUT"), t { "", "" }, m(1, l._1:match(l._1:reverse()), "PALINDROME") })
Inserts
"PALINDROME"if i(1) contains a palindrome. -
match(n, lambda._1:match("^" .. lambda._2 .. "$"), lambda._1:gsub("a", "e"))s("extras3", { i(1), t { "", "" }, i(2), t { "", "" }, m({ 1, 2 }, l._1:match("^" .. l._2 .. "$"), l._1:gsub("a", "e")) })
This inserts the text of the node with jump-index 1, with all occurrences of
areplaced withe, if the second insertNode matches the first exactly.
Inserts the text of the passed node.
rep(node_reference)
node_reference, a single Node Reference.
s("extras4", { i(1), t { "", "" }, extras.rep(1) })Evaluates a function on expand and inserts its value.
partial(fn, params...)
fn: any functionparams: varargs, any, will be passed tofn.
For example partial(os.date, "%Y") inserts the current year on expansion.
s("extras5", { extras.partial(os.date, "%Y") })Inserts text if the referenced node doesn't contain any text.
nonempty(node_reference, not_empty, empty):
node_reference, a single Node Reference.not_empty,string: inserted if the node is not empty.empty,string: inserted if the node is empty.
s("extras6", { i(1, ""), t { "", "" }, extras.nonempty(1, "not empty!", "empty!") })Pretty much the same as lambda, but it inserts the resulting text as an insertNode, and, as such, it can be quickly overridden.
dynamic_lambda(jump_indx, lambda, node_references)
jump_indx, as usual, the jump-index.
The remaining arguments carry over from lambda.
s("extras7", { i(1), t { "", "" }, extras.dynamic_lambda(2, l._1 .. l._1, 1) })Authoring snippets can be quite clunky, especially since every second node is
probably a textNode, inserting a small number of characters between two more
complicated nodes.
fmt can be used to define snippets in a much more readable way. This is
achieved by borrowing (as the name implies) from format-functionality (our
syntax is very similar to
python's).
fmt accepts a string and a table of nodes. Each occurrence of a delimiter pair
in the string is replaced by one node from the table, while text outside the
delimiters is turned into textNodes.
Simple example:
ls.add_snippets("all", {
-- important! fmt does not return a snippet, it returns a table of nodes.
s("example1", fmt("just an {iNode1}", {
iNode1 = i(1, "example")
})),
s("example2", fmt([[
if {} then
{}
end
]], {
-- i(1) is at nodes[1], i(2) at nodes[2].
i(1, "not now"), i(2, "when")
})),
s("example3", fmt([[
if <> then
<>
end
]], {
-- i(1) is at nodes[1], i(2) at nodes[2].
i(1, "not now"), i(2, "when")
}, {
delimiters = "<>"
})),
s("example4", fmt([[
repeat {a} with the same key {a}
]], {
a = i(1, "this will be repeat")
}, {
repeat_duplicates = true
}))
s("example5", fmt([[
line1: no indent
line3: 2 space -> 1 indent ('\t')
line4: 4 space -> 2 indent ('\t\t')
]], {}, {
indent_string = " "
}))
-- NOTE: [[\t]] means '\\t'
s("example6", fmt([[
line1: no indent
\tline3: '\\t' -> 1 indent ('\t')
\t\tline4: '\\t\\t' -> 2 indent ('\t\t')
]], {}, {
indent_string = [[\t]]
}))
})One important detail here is that the position of the delimiters does not, in any way, correspond to the jump-index of the nodes!
fmt(format:string, nodes:table of nodes, opts:table|nil) -> table of nodes
format: a string. Occurrences of{<somekey>}({}are customizable; more on that later) are replaced withcontent[<somekey>](which should be a node), while surrounding text becomestextNodes.
To escape a delimiter, repeat it ("{{").
If no key is given ({}) are numbered automatically:
"{} ? {} : {}"becomes"{1} ? {2} : {3}", while"{} ? {3} : {}"becomes"{1} ? {3} : {4}"(the count restarts at each numbered placeholder). If a key appears more than once informat, the node incontent[<duplicate_key>]is inserted for the first, and copies of it for subsequent occurrences.nodes: just a table of nodes.opts: optional arguments:delimiters: string, two characters. Change{}to some other pair, e.g."<>".strict: Warn about unused nodes (default true).trim_empty: remove empty ("%s*") first and last line informat. Useful when passing multiline strings via[[]](default true).dedent: remove indent common to all lines informat. Again, makes passing multiline-strings a bit nicer (default true).indent_string: convertindent_stringat beginning of each line to unit indent ('\t'). This is applied afterdedent. Useful when using multiline string infmt. (default empty string, disabled)repeat_duplicates: repeat nodes when a key is reused instead of copying the node if it has a jump-index, refer to Basics-Jump-Index to know which nodes have a jump-index (default false).
There is also require("luasnip.extras.fmt").fmta. This only differs from fmt
by using angle brackets (<>) as the default delimiter.
This module (luasnip.extras.condition) contains functions that can be passed to
a snippet's condition or show_condition. These are grouped accordingly into
luasnip.extras.conditions.expand and luasnip.extras.conditions.show:
expand:
line_begin: only expand if the cursor is at the beginning of the line.trigger_not_preceded_by(pattern): only expand if the character before the trigger does not matchpattern. This is a generalization ofwordTrig, which can be implemented astrigger_not_preceded_by("[%w_]"), and is available asword_trig_condition.
show:
line_end: only expand at the end of the line.has_selected_text: only expand if there's selected text stored after pressingstore_selection_keys.
Additionally, expand contains all conditions provided by show.
luasnip.extras.conditions also contains condition objects. These can, just
like functions, be passed to condition or show_condition, but can also be
combined with each other into logical expressions:
-c1 -> not c1c1 * c2 -> c1 and c2c1 + c2 -> c1 or c2c1 - c2 -> c1 and not c2: This is similar to set differences:A \ B = {a in A | a not in B}. This makes-(a + b) = -a - ban identity representing de Morgan's law:not (a or b) = not a and not b. However, since boolean algebra lacks an additive inverse,a + (-b) = a - bdoes not hold. Thus, this is NOT the same asc1 + (-c2).c1 ^ c2 -> c1 xor(!=) c2c1 % c2 -> c1 xnor(==) c2: This decision may seem weird, considering how there is an overload for the==-operator. Unfortunately, it's not possible to use this for our purposes (some info here), so we decided to make use of a more obscure symbol (which will hopefully avoid false assumptions about its meaning).
This makes logical combinations of conditions very readable. Compare
condition = conditions.expand.line_end + conditions.expand.line_beginwith the more verbose
condition = function(...) return conditions.expand.line_end(...) or conditions.expand.line_begin(...) endThe conditions provided in show and expand are already condition objects. To
create new ones, use
require("luasnip.extras.conditions").make_condition(condition_fn)
Sometimes it's desirable to create snippets tailored for exactly the current situation. For example inserting repetitive, but just slightly different invocations of some function, or supplying data in some schema.
On-the-fly snippets enable exactly this use case: they can be quickly created and expanded with as little disruption as possible.
Since they should mainly fast to write and don't necessarily need all bells and
whistles, they don't make use of lsp/textmate-syntax, but a more simplistic one:
$anytextdenotes a placeholder (insertNode) with text "anytext". The text also serves as a unique key: if there are multiple placeholders with the same key, only the first will be editable, the others will just mirror it.- ... That's it.
$can be escaped by preceding it with a second$, all other symbols will be interpreted literally.
There is currently only one way to expand on-the-fly snippets:
require('luasnip.extras.otf').on_the_fly("<some-register>") will interpret
whatever text is in the register <some-register> as a snippet, and expand it
immediately.
The idea behind this mechanism is that it enables a very immediate way of
supplying and retrieving (expanding) the snippet: write the snippet-body into
the buffer, cut/yank it into some register, and call on_the_fly("<register>")
to expand the snippet.
Here's one set of example keybindings:
" in the first call: passing the register is optional since `on_the_fly`
" defaults to the unnamed register, which will always contain the previously cut
" text.
vnoremap <c-f> "ec<cmd>lua require('luasnip.extras.otf').on_the_fly("e")<cr>
inoremap <c-f> <cmd>lua require('luasnip.extras.otf').on_the_fly("e")<cr>Obviously, <c-f> is arbitrary and can be changed to any other key combo.
Another interesting application is allowing multiple on-the-fly snippets at the
same time by retrieving snippets from multiple registers:
" For register a
vnoremap <c-f>a "ac<cmd>lua require('luasnip.extras.otf').on_the_fly()<cr>
inoremap <c-f>a <cmd>lua require('luasnip.extras.otf').on_the_fly("a")<cr>
" For register b
vnoremap <c-f>a "bc<cmd>:lua require('luasnip.extras.otf').on_the_fly()<cr>
inoremap <c-f>b <cmd>lua require('luasnip.extras.otf').on_the_fly("b")<cr>It's possible to leverage vim.ui.select for selecting a choice directly,
without cycling through the available choices.
All that is needed for this is calling
require("luasnip.extras.select_choice"), most likely via some keybinding, e.g.
inoremap <c-u> <cmd>lua require("luasnip.extras.select_choice")()<cr>while inside a choiceNode.
The opts.kind hint for vim.ui.select will be set to luasnip.
Contains some utility functions that can be passed to the ft_func or
load_ft_func-settings.
-
from_filetype: the default forft_func. Simply returns the filetype(s) of the buffer. -
from_cursor_pos: uses tree-sitter to determine the filetype at the cursor. With that, it's possible to expand snippets in injected regions, as long as the tree-sitter parser supports them. If this is used in conjunction withlazy_load, extra care must be taken that all the filetypes that can be expanded in a given buffer are also returned byload_ft_func(otherwise their snippets may not be loaded). This can easily be achieved withextend_load_ft. -
extend_load_ft:fn(extend_ft:map) -> fnA simple solution to the problem described above is loading more filetypes than just that of the target buffer whenlazy_loading. This can be done ergonomically viaextend_load_ft: calling it with a table where the keys are filetypes, and the values are the filetypes that should be loaded additionally returns a function that can be passed toload_ft_funcand takes care of extending the filetypes properly.ls.setup({ load_ft_func = -- Also load both lua and json when a markdown-file is opened, -- javascript for html. -- Other filetypes just load themselves. require("luasnip.extras.filetype_functions").extend_load_ft({ markdown = {"lua", "json"}, html = {"javascript"} }) })
Postfix snippets, famously used in
rust analyzer and various IDEs, are a type
of snippet which alters text before the snippet's trigger. While these
can be implemented using regTrig snippets, this helper makes the process easier
in most cases.
The simplest example, which surrounds the text preceding the .br with
brackets [], looks like:
postfix(".br", {
f(function(_, parent)
return "[" .. parent.snippet.env.POSTFIX_MATCH .. "]"
end, {}),
})and is triggered with xxx.br and expands to [xxx].
Note the parent.snippet.env.POSTFIX_MATCH in the function node. This is additional
field generated by the postfix snippet. This field is generated by extracting
the text matched (using a configurable matching string, see below) from before
the trigger. In the case above, the field would equal "xxx". This is also
usable within dynamic nodes.
This field can also be used within lambdas and dynamic nodes.
postfix(".br", {
l("[" .. l.POSTFIX_MATCH .. "]"),
})postfix(".brd", {
d(1, function (_, parent)
return sn(nil, {t("[" .. parent.env.POSTFIX_MATCH .. "]")})
end)
})The arguments to postfix are identical to the arguments to s but with a few
extra options.
The first argument can be either a string or a table. If it is a string, that
string will act as the trigger, and if it is a table it has the same valid keys
as the table in the same position for s except:
wordTrig: This key will be ignored if passed in, as it must always be false for postfix snippets.match_pattern: The pattern that the line before the trigger is matched against. The default match pattern is"[%w%.%_%-]+$". Note the$. This matches since only the line up until the beginning of the trigger is matched against the pattern, which makes the character immediately preceding the trigger match as the end of the string.
Some other match strings, including the default, are available from the postfix
module. require("luasnip.extras.postfix).matches:
default:[%w%.%_%-%"%']+$line:^.+$
The second argument is identical to the second argument for s, that is, a
table of nodes.
The optional third argument is the same as the third (opts) argument to the
s function, but with one difference:
The postfix snippet works using a callback on the pre_expand event of the snippet. If you pass a callback on the pre_expand event (structure example below) it will get run after the builtin callback.
{
callbacks = {
[-1] = {
[events.pre_expand] = function(snippet, event_args)
-- function body to match before the dot
-- goes here
end
}
}
}Instead of triggering a postfix-snippet when some pattern matches in front of
the trigger, it might be useful to match if some specific tree-sitter nodes
surround/are in front of the trigger.
While this functionality can also be implemented by a custom
resolveExpandParams, this helper simplifies the common cases.
This matching of tree-sitter nodes can be done either
- by providing a query and the name of the capture that should be in front of the trigger (in most cases, the complete match, but requiring specific nodes before/after the matched node may be useful as well), or
- by providing a function that manually walks the node-tree, and returns the node in front of the trigger on success (for increased flexibility).
A simple example, which surrounds the previous node's text preceding the .mv
with std::move() in C++ files, looks like:
local treesitter_postfix = require("luasnip.extras.treesitter_postfix").treesitter_postfix
treesitter_postfix({
trig = ".mv",
matchTSNode = {
query = [[
[
(call_expression)
(identifier)
(template_function)
(subscript_expression)
(field_expression)
(user_defined_literal)
] @prefix
]]
query_lang = "cpp"
},
},{
f(function(_, parent)
local node_content = table.concat(parent.snippet.env.LS_TSMATCH, '\n')
local replaced_content = ("std::move(%s)"):format(node_content)
return vim.split(ret_str, "\n", { trimempty = false })
end)
})LS_TSMATCH is the tree-sitter-postfix equivalent to POSTFIX_MATCH, and is
populated with the match (in this case the text of a tree-sitter-node) in front
of the trigger.
The arguments to treesitter_postfix are identical to the arguments to s but
with a few extra options.
The first argument has to be a table, which defines at least trig and
matchTSNode. All keys from the regular s may be set here (except for
wordTrig, which will be ignored), and additionally the following:
reparseBuffer,string?: Sometimes the trigger may interfere with tree-sitter recognizing queries correctly. With this option, the trigger may either be removed from the live-buffer ("live"), from a copy of the buffer ("copy"), or not at all (nil).matchTSNode: How to determine whether there is a matching node in front of the cursor. There are two options:fun(parser: LuaSnip.extra.TSParser, pos: { [1]: number, [2]: number }): LuaSnip.extra.NamedTSMatch?, TSNode?Manually determine whether there is a matching node that ends just beforepos(the beginning of the trigger).
Returnnil,nilif there is no match, otherwise first return a table mapping names to nodes (the text, position and type of these will be provided viasnip.env), and second the node that is the matched node.LuaSnip.extra.MatchTSNodeOpts, which represents a query and provides all captures of the matched pattern inNamedTSMatch. It contains the following options:query,string: The query, in textual form.query_name,string: The name of the runtime-query to be used (passed toquery.get()), defaults to"luasnip"(so one could create a file which only contains queries used by luasnip, like$CONFDIR/queries/<lang>/luasnip.scm, which might make sense to define general concepts independent of a single snippet).
queryandquery_nameare mutually exclusive, only one of both shall be defined.query_lang,string: The language of the query. This is the only required parameter to this function, since there's no sufficiently straightforward way to determine the language of the query for us. Consider usingextend_overrideto define ats_postfix-function that automatically fills in the language for the filetype of the snippet-file.match_captures,string|string[]: The capture(s) to use for determining the actual prefix (so the node that should be immediately in front of the trigger). This defaults to just"prefix".select,string?|fun(): LuaSnip.extra.MatchSelector: Since there may be multiple matching captures in front of the cursor, there has to be some way to select the node that will actually be used.
If this is a string, it has to be one of "any", "shortest", or "longest", which mean that any, the shortest, or the longest match is used.
If it is a function, it must return a table with two fields,recordandretrieve.recordis called with aTSMatchand a potential node for theTSMatch, and may returntrueto abort the selection-procedure.retrievemust return either aTSMatch-TSNode-tuple (which is used as the match) ornil, to signify that there is no match.
lua/luasnip/extras/_treesitter.luacontains the tablebuiltin_tsnode_selectors, which contains the implementations for any/shortest/longest, which can be used as examples for more complicated custom-selectors.
The text of the matched node can be accessed as snip.env.LS_TSMATCH.
The text of the nodes returned as NamedTSMatch can be accessed as
snip.env.LS_TSCAPTURE_<node-name-in-caps>, and their range and type as
snip.env.LS_TSDATA.<node-name-NOT-in-caps>.range/type (where range is a
tuple of row-col-tuples, both 0-indexed).
For a query like
(function_declaration
name: (identifier) @fname
parameters: (parameters) @params
body: (block) @body
) @prefixmatched against
function add(a, b)
return a + b
endsnip.env would contain:
LS_TSMATCH:{ "function add(a, b)", "\treturn a + b", "end" }LS_TSDATA:{ body = { range = { { 1, 1 }, { 1, 13 } }, type = "block" }, fname = { range = { { 0, 9 }, { 0, 12 } }, type = "identifier" }, params = { range = { { 0, 12 }, { 0, 18 } }, type = "parameters" }, prefix = { range = { { 0, 0 }, { 2, 3 } }, type = "function_declaration" } }LS_TSCAPTURE_FNAME:{ "add" }LS_TSCAPTURE_PARAMS:{ "(a, b)" }LS_TSCAPTURE_BODY:{ "return a + b" }LS_TSCAPTURE_PREFIX:{ "function add(a, b)", "\treturn a + b", "end" }
(note that all variables containing text of nodes are string-arrays, one entry for each line)
There is one important caveat when accessing LS_TSDATA in
function/dynamicNodes: It won't contain the values as specified here while
generating docstrings (in fact, it won't even be a table).
Since docstrings have to be generated without any runtime-information, we just
have to provide dummy-data in env, which will be some kind of string related
to the name of the environment variable.
Since the structure of LS_TSDATA obviously does not fit that model, we can't
really handle it in a nice way (at least yet). So, for now, best include a check
like local static_evaluation = type(env.LS_TSDATA) == "string", and behave
accordingly if static_evaluation is true (for example, return some value
tailored for displaying it in a docstring).
One more example, which actually uses a few captures:
ts_post({
matchTSNode = {
query = [[
(function_declaration
name: (identifier) @fname
parameters: (parameters) @params
body: (block) @body
) @prefix
]],
query_lang = "lua",
},
trig = ".var"
}, fmt([[
local {} = function{}
{}
end
]], {
l(l.LS_TSCAPTURE_FNAME),
l(l.LS_TSCAPTURE_PARAMS),
l(l.LS_TSCAPTURE_BODY),
}))The module luasnip.extras.treesitter_postfix contains a few functions that may
be useful for creating more efficient ts-postfix-snippets.
Nested in builtin.tsnode_matcher are:
fun find_topmost_types(types: string[]): MatchTSNodeFunc: Generates aLuaSnip.extra.MatchTSNodeFuncwhich returns the last parent whose type is intypes.fun find_first_types(types: string[]): MatchTSNodeFunc: Similar tofind_topmost_types, only this one matches the first parent whose type is in types.find_nth_parent(n: number): MatchTSNodeFunc: Simply matches then-th parent of the innermost node in front of the trigger.
With find_topmost_types, the first example can be implemented more
efficiently (without needing a whole query):
local postfix_builtin = require("luasnip.extras.treesitter_postfix").builtin
ls.add_snippets("all", {
ts_post({
matchTSNode = postfix_builtin.tsnode_matcher.find_topmost_types({
"call_expression",
"identifier",
"template_function",
"subscript_expression",
"field_expression",
"user_defined_literal"
}),
trig = ".mv"
}, {
l(l_str.format("std::move(%s)", l.LS_TSMATCH))
})
}, {key = "asdf"})local sl = require("luasnip.extras.snippet_list")Makes an open function available to use to open currently available snippets
in a different buffer/window/tab.
sl.open(opts:table|nil)
opts: optional arguments:snip_info:snip_info(snippet) -> table representation of snippetprinter:printer(snippets:table) -> anydisplay:display(snippets:any)
Benefits include: syntax highlighting, searching, and customizability.
Simple Example:
sl.open()Customization Examples:
-- making our own snip_info
local function snip_info(snippet)
return { name = snippet.name }
end
-- using it
sl.open({snip_info = snip_info})-- making our own printer
local function printer(snippets)
local res = ""
for ft, snips in pairs(snippets) do
res = res .. ft .. "\n"
for _, snip in pairs(snips) do
res = res .. " " .. "Name: " .. snip.name .. "\n"
res = res .. " " .. "Desc: " .. snip.description[1] .. "\n"
res = res .. " " .. "Trigger: " .. snip.trigger .. "\n"
res = res .. " ----" .. "\n"
end
end
return res
end
-- using it
sl.open({printer = printer})-- making our own display
local function display(printer_result)
-- right vertical split
vim.cmd("botright vnew")
-- get buf and win handle
local buf = vim.api.nvim_get_current_buf()
local win = vim.api.nvim_get_current_win()
-- setting window and buffer options
vim.api.nvim_win_set_option(win, "foldmethod", "manual")
vim.api.nvim_buf_set_option(buf, "filetype", "javascript")
vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
vim.api.nvim_buf_set_option(buf, "buflisted", false)
vim.api.nvim_buf_set_name(buf, "Custom Display buf " .. buf)
-- dump snippets
local replacement = vim.split(printer_result)
vim.api.nvim_buf_set_lines(buf, 0, 0, false, replacement)
end
-- using it
sl.open({display = display})There is a caveat with implementing your own printer and/or display function. The default behavior for the printer function is to return a string representation of the snippets. The display function uses the results from the printer function, therefore by default the display function is expecting that result to be a string.
However, this doesn't have to be the case. For example, you can implement your own printer function that returns a table representation of the snippets but you would have to then implement your own display function or some other function in order to return the result as a string.
An options table, which has some core functionality that can be used
to customize 'common' settings, is provided.
sl.options: options table:display:display(opts:table|nil) -> function(printer_result:string)
You can see from the example above that making a custom display is a fairly
involved process. What if you just wanted to change a buffer option like the
name or just the filetype? This is where sl.options.display comes in. It
allows you to customize buffer and window options while keeping the default
behavior.
sl.options.display(opts:table|nil) -> function(printer_result:string)
opts: optional arguments:win_opts:table which has a {window_option = value} formbuf_opts:table which has a {buffer_option = value} formget_name:get_name(buf) -> string
Let's recreate the custom display example above:
-- keeping the default display behavior but modifying window/buffer
local modified_default_display = sl.options.display({
buf_opts = {filetype = "javascript"},
win_opts = {foldmethod = "manual"},
get_name = function(buf) return "Custom Display buf " .. buf end
})
-- using it
sl.open({display = modified_default_display})This module can consume a snippets source, more specifically, jump to
the location referred by it.
This is primarily implemented for snippet which got their source from one of the
loaders, but might also work for snippets where the source was set manually.
require("luasnip.extras.snip_location"):
snip_location.jump_to_snippet(snip, opts)Jump to the definition ofsnip.snip: a snippet with attached source-data.opts:nil|table, optional arguments, valid keys are:hl_duration_ms:number, duration for which the definition should be highlighted, in milliseconds. 0 disables the highlight.edit_fn:function(file), this function will be called with the file the snippet is located in, and is responsible for jumping to it.
We assume that after it has returned, the current buffer containsfile.
snip_location.jump_to_active_snippet(opts)Jump to definition of active snippet.opts:nil|table, accepts the same keys as theopts-parameter ofjump_to_snippet.
Most of LuaSnip's functions have some arguments to control their behavior.
Examples include s, where wordTrig, regTrig, ... can be set in the first
argument to the function, or fmt, where the delimiter can be set in the third
argument.
This is all good and well, but if these functions are often used with
non-default settings, it can become cumbersome to always explicitly set them.
This is where the extend_decorator comes in:
it can be used to create decorated functions which always extend the arguments
passed directly with other previously defined ones.
An example:
local fmt = require("luasnip.extras.fmt").fmt
fmt("{}", {i(1)}) -- -> list of nodes, containing just the i(1).
-- when authoring snippets for some filetype where `{` and `}` are common, they
-- would always have to be escaped in the format-string. It might be preferable
-- to use other delimiters, like `<` and `>`.
fmt("<>", {i(1)}, {delimiters = "<>"}) -- -> same as above.
-- but it's quite annoying to always pass the `{delimiters = "<>"}`.
-- with extend_decorator:
local fmt_angle = ls.extend_decorator.apply(fmt, {delimiters = "<>"})
fmt_angle("<>", {i(1)}) -- -> same as above.
-- the same also works with other functions provided by luasnip, for example all
-- node/snippet-constructors and `parse_snippet`.extend_decorator.apply(fn, ...) requires that fn is previously registered
via extend_decorator.register.
(This is not limited to LuaSnip's functions; although, for usage outside of
LuaSnip, best copy the source file: /lua/luasnip/util/extend_decorator.lua).
register(fn, ...):
fn: the function....: any number of tables. Each specifies how to extend an argument offn. The tables accept:arg_indx,number(required): the position of the parameter to override.extend,fn(arg, extend_value) -> effective_arg(optional): this function is used to extend the arguments passed to the decorated function. It defaults to a function which just extends the arguments table with the extend table (acceptsnil). This extend behavior is adaptable to accommodates, where the first argument may be string or table.
apply(fn, ...) -> decorated_fn:
fn: the function to decorate....: The values to extend with. These should match the descriptions passed inregister(the argument first passed toregisterwill be extended with the first value passed here).
One more example for registering a new function:
local function somefn(arg1, arg2, opts1, opts2)
-- not important
end
-- note the reversed arg_indx!!
extend_decorator.register(somefn, {arg_indx=4}, {arg_indx=3})
local extended = extend_decorator.apply(somefn,
{key = "opts2 is extended with this"},
{key = "and opts1 with this"})
extended(...)LuaSnip is capable of parsing LSP-style snippets using
ls.parser.parse_snippet(context, snippet_string, opts):
ls.parser.parse_snippet({trig = "lsp"}, "$1 is ${2|hard,easy,challenging|}")context can be:
string|table: treated like the first argument tols.s,parse_snippetreturns a snippet.number:parse_snippetreturns a snippetNode, with the positioncontext.nil:parse_snippetreturns a flat table of nodes. This can be used likefmt.
Nested placeholders("${1:this is ${2:nested}}") will be turned into
choiceNodes with:
- the given snippet(
"this is ${1:nested}") and - an empty insertNode
This behavior can be modified by changing parser_nested_assembler in
ls.setup().
LuaSnip will also modify some snippets that it is incapable of representing accurately:
- if the
$0is a placeholder with something other than just text inside - if the
$0is a choice - if the
$0is not an immediate child of the snippet (it could be inside a placeholder:"${1: $0 }")
To remedy those incompatibilities, the invalid $0 will be replaced with a
tabstop/placeholder/choice which will be visited just before the new $0. This
new $0 will be inserted at the (textually) earliest valid position behind the
invalid $0.
opts can contain the following keys:
trim_empty: boolean, remove empty lines from the snippet. Default true.dedent: boolean, remove common indent from the snippet's lines. Default true.
Both trim_empty and dedent will be disabled for snippets parsed via
ls.lsp_expand: it might prevent correct expansion of snippets sent by LSP.
It is furthermore possible to parse SnipMate snippets (this includes support for Vim script-evaluation!!)
SnipMate snippets need to be parsed with a different function,
ls.parser.parse_snipmate:
ls.parser.parse_snipmate("year", "The year is `strftime('%Y')`")parse_snipmate accepts the same arguments as parse_snippet, only the
snippet body is parsed differently.
To apply Variable/Placeholder-transformations, LuaSnip needs to apply ECMAScript regular expressions. This is implemented by relying on jsregexp.
The easiest (but potentially error-prone) way to install it is by calling make install_jsregexp in the repository root.
This process can be automated by packer.nvim:
use { "L3MON4D3/LuaSnip", run = "make install_jsregexp" }If this fails, first open an issue :P, and then try installing the
jsregexp-LuaRock. This is also possible via
packer.nvim, although actual usage may require a small workaround, see
here or
here.
Alternatively, jsregexp can be cloned locally, maked, and the resulting
jsregexp.so placed in some place where Neovim can find it (probably
~/.config/nvim/lua/).
If jsregexp is not available, transformations are replaced by a simple copy.
All TM_something-variables are supported with two additions:
LS_SELECT_RAW and LS_SELECT_DEDENT. These were introduced because
TM_SELECTED_TEXT is designed to be compatible with VSCode's behavior, which
can be counterintuitive when the snippet can be expanded at places other than
the point where selection started (or when doing transformations on selected text).
Besides those we also provide LS_TRIGGER which contains the trigger of the snippet,
and LS_CAPTURE_n (where n is a positive integer) that contains the n-th capture
when using a regex with capture groups as trig in the snippet definition.
All variables can be used outside of LSP parsed snippets as their values are
stored in a snippets' snip.env-table:
s("selected_text", f(function(args, snip)
local res, env = {}, snip.env
table.insert(res, "Selected Text (current line is " .. env.TM_LINE_NUMBER .. "):")
for _, ele in ipairs(env.LS_SELECT_RAW) do table.insert(res, ele) end
return res
end, {}))To use any *SELECT* variable, the store_selection_keys must be set via
require("luasnip").config.setup({store_selection_keys="<Tab>"}). In this case,
hitting <Tab> while in visual mode will populate the *SELECT*-vars for the next
snippet and then clear them.
You can also add your own variables by using the ls.env_namespace(name, opts) where:
name:stringthe names the namespace, can't contain the character "_"optsis a table containing (in every caseEnvValis the same asstring|list[string]:vars:(fn(name:string)->EnvVal) | map[string, EnvVal]Is a function that receives a string and returns a value for the var with that name or a table from var name to a value (in this case, if the value is a function it will be executed lazily once per snippet expansion).init:fn(info: table)->map[string, EnvVal]Returns a table of variables that will set to the environment of the snippet on expansion, use this for vars that have to be calculated in that moment or that depend on each other. Theinfotable argument containspos(0-based position of the cursor on expansion), thetriggerof the snippet and thecaptureslist.eager:list[string]names of variables that will be taken fromvarsand appended eagerly (like those ininit)multiline_vars:(fn(name:string)->bool)|map[string, bool]|bool|string[]Says if certain vars are a table or just a string, can be a function that get's the name of the var and returns true if the var is a key, a list of vars that are tables or a boolean for the full namespace, it's false by default. Refer to issue#510 for more information.
The four fields of opts are optional but you need to provide either init or vars, and eager can't be without vars.
Also, you can't use namespaces that override default vars.
A simple example to make it more clear:
local function random_lang()
return ({"LUA", "VIML", "VIML9"})[math.floor(math.random()/2 + 1.5)]
end
ls.env_namespace("MY", {vars={ NAME="LuaSnip", LANG=random_lang }})
-- then you can use $MY_NAME and $MY_LANG in your snippets
ls.env_namespace("SYS", {vars=os.getenv, eager={"HOME"}})
-- then you can use $SYS_HOME which was eagerly initialized but also $SYS_USER (or any other system environment var) in your snippets
lsp.env_namespace("POS", {init=function(info) return {VAL=vim.inspect(info.pos)} end})
-- then you can use $POS_VAL in your snippets
s("custom_env", d(1, function(args, parent)
local env = parent.snippet.env
return sn(nil, t {
"NAME: " .. env.MY_NAME,
"LANG: " .. env.MY_LANG,
"HOME: " .. env.SYS_HOME,
"USER: " .. env.SYS_USER,
"VAL: " .. env.POS_VAL
})
end, {}))All variables, even ones added via env_namespace, can be accessed in
LSP snippets as $VAR_NAME.
The LSP specification states:
With $name or ${name:default} you can insert the value of a variable.
When a variable isn't set, its default or the empty string is inserted.
When a variable is unknown (that is, its name isn't defined) the name of the variable is inserted and it is transformed into a placeholder.
The above necessitates a differentiation between unknown and unset variables:
For LuaSnip, a variable VARNAME is unknown when env.VARNAME returns nil and unset
if it returns an empty string.
Consider this when adding environment variables which might be used in LSP snippets.
Luasnip is capable of loading snippets from different formats, including both the well-established VSCode and SnipMate format, as well as plain Lua files for snippets written in Lua.
All loaders (except the vscode-standalone-loader) share a similar interface:
require("luasnip.loaders.from_{vscode,snipmate,lua}").{lazy_,}load(opts:table|nil)
where opts can contain the following keys:
paths: List of paths to load. Can be a table, or a single comma-separated string. The paths may begin with~/or./to indicate that the path is relative to your$HOMEor to the directory where your$MYVIMRCresides (useful to add your snippets).
If not set,runtimepathis searched for directories that contain snippets. This procedure differs slightly for each loader:lua: the snippet-library has to be in a directory named"luasnippets".snipmate: similar to Lua, but the directory has to be"snippets".vscode: any directory inruntimepaththat contains apackage.jsoncontributing snippets.
lazy_paths: behaves essentially likepaths, with two exceptions: if it isnil, it does not default toruntimepath, and the paths listed here do not need to exist, and will be loaded on creation.
LuaSnip will do its best to determine the path that this should resolve to, but since the resolving we do is not very sophisticated it may produce incorrect paths. Definitely check the log if snippets are not loaded as expected.exclude: List of languages to exclude, empty by default.include: List of languages to include, includes everything by default.{override,default}_priority: These keys are passed straight to theadd_snippets-calls (documented in API) and can therefore change the priority of snippets loaded from some collection (or, in combination with{in,ex}clude, only some of its snippets).fs_event_providers:table<string, boolean>?, specifies which mechanisms should be used to watch files for updates/creation.
Ifautocmdis set totrue, aBufWritePost-hook watches files of this collection, iflibuvis set, thefile-watcher-apiexposed bylibuvis used to watch for updates.
Uselibuvif you want snippets to update from other Neovim-instances, andautocmdif the collection resides on a file system where thelibuv-watchers may not work correctly. Or, of course, just enable both :D
By default, onlyautocmdis enabled.
While load will immediately load the snippets, lazy_load will defer loading until
the snippets are actually needed (whenever a new buffer is created, or the
filetype is changed LuaSnip actually loads lazy_loaded snippets for the
filetypes associated with this buffer. This association can be changed by
customizing load_ft_func in setup: the option takes a function that, passed
a bufnr, returns the filetypes that should be loaded (fn(bufnr) -> filetypes (string[]))).
All of the loaders support reloading, so simply editing any file contributing
snippets will reload its snippets (according to fs_event_providers in the
instance where the file was edited, or in other instances as well).
As an alternative (or addition) to automatic reloading, LuaSnip can also process
manual updates to files: Call require("luasnip.loaders").reload_file(path) to
reload the file at path.
This may be useful when the collection is controlled by some other plugin, or
when enabling the other reload-mechanisms is for some reason undesirable
(performance? minimalism?).
For easy editing of these files, LuaSnip provides a vim.ui.select-based dialog
(Loaders-edit_snippets) where first the filetype, and then the
file can be selected.
Some loaders (vscode,lua) support giving snippets generated in some file their
own filetype (vscode via scope, lua via the underlying filetype-option for
snippets). These snippet-specific filetypes are not considered when determining
which files to lazy_load for some filetype, this is exclusively determined by
the language associated with a file in vscodes' package.json, and the
file/directory-name in lua.
- This can be resolved relatively easily in
vscode, where thelanguageadvertised inpackage.jsoncan just be a superset of thescopes in the file. - Another simplistic solution is to set the language to
all(inlua, it might make sense to create a directoryluasnippets/all/*.luato group these files together). - Another approach is to modify
load_ft_functo load a custom filetype if the snippets should be activated, and store the snippets in a file for that filetype. This can be used to group snippets by e.g. framework, and load them once a file belonging to such a framework is edited.
Example:
react.lua
return {
s({filetype = "css", trig = ...}, ...),
s({filetype = "html", trig = ...}, ...),
s({filetype = "js", trig = ...}, ...),
}luasnip_config.lua
load_ft_func = function(bufnr)
if "<bufnr-in-react-framework>" then
-- will load `react.lua` for this buffer
return {"react"}
else
return require("luasnip.extras.filetype_functions").from_filetype_load
end
endSee the Troubleshooting-Adding Snippets-Loaders section if one is having issues adding snippets via loaders.
As a reference on the structure of these snippet libraries, see friendly-snippets.
We support a small extension: snippets can contain LuaSnip-specific options in
the luasnip-table:
"example1": {
"prefix": "options",
"body": [
"whoa! :O"
],
"luasnip": {
"priority": 2000,
"autotrigger": true,
"wordTrig": false
}
}Files with the extension jsonc will be parsed as jsonc,
json with comments,
while *.json are parsed with a regular json parser, where comments are
disallowed. (the json parser is a bit faster, so don't default to jsonc if
it's not necessary).
Example:
~/.config/nvim/my_snippets/package.json:
{
"name": "example-snippets",
"contributes": {
"snippets": [
{
"language": [
"all"
],
"path": "./snippets/all.json"
},
{
"language": [
"lua"
],
"path": "./lua.json"
}
]
}
}~/.config/nvim/my_snippets/snippets/all.json:
{
"snip1": {
"prefix": "all1",
"body": [
"expands? jumps? $1 $2 !"
]
},
"snip2": {
"prefix": "all2",
"body": [
"multi $1",
"line $2",
"snippet$0"
]
}
}~/.config/nvim/my_snippets/lua.json:
{
"snip1": {
"prefix": "lua",
"body": [
"lualualua"
]
}
}This collection can be loaded with any of
-- don't pass any arguments, luasnip will find the collection because it is
-- (probably) in rtp.
require("luasnip.loaders.from_vscode").lazy_load()
-- specify the full path...
require("luasnip.loaders.from_vscode").lazy_load({paths = "~/.config/nvim/my_snippets"})
-- or relative to the directory of $MYVIMRC
require("luasnip.loaders.from_vscode").load({paths = "./my_snippets"})Beside snippet-libraries provided by packages, vscode also supports another
format which can be used for project-local snippets, or user-defined snippets,
.code-snippets.
The layout of these files is almost identical to that of the package-provided
snippets, but there is one additional field supported in the
snippet-definitions, scope, with which the filetype of the snippet can be set.
If scope is not set, the snippet will be added to the global filetype (all).
require("luasnip.loaders.from_vscode").load_standalone(opts)
opts:table, can contain the following keys:path:string, Path to the*.code-snippets-file that should be loaded. Just like the paths inload, this one can begin with a"~/"to be relative to$HOME, and a"./"to be relative to the Neovim config directory.{override,default}_priority: These keys are passed straight to theadd_snippets-calls (documented in API) and can be used to change the priority of the loaded snippets.lazy:boolean, if it is set, the file does not have to exist whenload_standaloneis called, and it will be loaded on creation.
falseby default.
Example:
a.code-snippets:
This file can be loaded by calling
require("luasnip.loaders.from_vscode").load_standalone({path = "a.code-snippets"})Luasnip does not support the full SnipMate format: Only ./{ft}.snippets and
./{ft}/*.snippets will be loaded. See
honza/vim-snippets for lots of
examples.
Like VSCode, the SnipMate format is also extended to make use of some of LuaSnip's more advanced capabilities:
priority 2000
autosnippet options
whoa :OExample:
~/.config/nvim/snippets/c.snippets:
# this is a comment
snippet c c-snippet
c!~/.config/nvim/snippets/cpp.snippets:
extends c
snippet cpp cpp-snippet
cpp!This can, again, be loaded with any of
require("luasnip.loaders.from_snipmate").load()
-- specify the full path...
require("luasnip.loaders.from_snipmate").lazy_load({paths = "~/.config/nvim/snippets"})
-- or relative to the directory of $MYVIMRC
require("luasnip.loaders.from_snipmate").lazy_load({paths = "./snippets"})Stuff to watch out for:
- Using both
extends <ft2>in<ft1>.snippetsandls.filetype_extend("<ft1>", {"<ft2>"})leads to duplicate snippets. ${VISUAL}will be replaced by$TM_SELECTED_TEXTto make the snippets compatible with LuaSnip- We do not implement
evalusing ` (backtick). This may be implemented in the future.
Instead of adding all snippets via add_snippets, it's possible to store them
in separate files and load all of those.
The file-structure here is exactly the supported SnipMate-structure, e.g.
<ft>.lua or <ft>/*.lua to add snippets for the filetype <ft>.
There are two ways to add snippets:
- the files may return two lists of snippets, the snippets in the first are all
added as regular snippets, while the snippets in the second will be added as
autosnippets (both are the defaults, if a snippet defines a different
snippetType, that will have preference) - snippets can also be appended to the global (only for these files - they are not
visible anywhere else) tables
ls_file_snippetsandls_file_autosnippets. This can be combined with a customsnip_envto define and add snippets with one function call:This is more flexible than the previous approach since the snippets don't have to be collected; they just have to be defined using the abovels.setup({ snip_env = { s = function(...) local snip = ls.s(...) -- we can't just access the global `ls_file_snippets`, since it will be -- resolved in the environment of the scope in which it was defined. table.insert(getfenv(2).ls_file_snippets, snip) end, parse = function(...) local snip = ls.parser.parse_snippet(...) table.insert(getfenv(2).ls_file_snippets, snip) end, -- remaining definitions. ... }, ... })
sandparse.
As defining all of the snippet constructors (s, c, t, ...) in every file
is rather cumbersome, LuaSnip will bring some globals into scope for executing
these files.
By default, the names from luasnip.config.snip_env will be used, but it's
possible to customize them by setting snip_env in setup.
Example:
~/snippets/all.lua:
return {
s("trig", t("loaded!!"))
}~/snippets/c.lua:
return {
s("ctrig", t("also loaded!!"))
}, {
s("autotrig", t("autotriggered, if enabled"))
}Load via
require("luasnip.loaders.from_lua").load({paths = "~/snippets"})One side-effect of the injected globals is that language servers, for example
lua-language-server, do not know about them, which means that snippet-files
may have many diagnostics about missing symbols.
There are a few ways to fix this
-
Add all variables in
snip_envtoLua.diagnostic.globals:-- wherever your lua-language-server lsp settings are defined: settings = { Lua = { ... diagnostics = { globals = { "vim", "s", "c", "t", ... } } } }
This will disable the warnings, but will do so in all files these lsp-settings are used with.
Similarly, adding---@diagnostic disable: undefined-globalto the snippet-files is also possible, but this affects not only the variables insnip_env, but all variables, like local variable names that may be mistyped. -
A more complete, and only slightly more complicated solution is using
lua-language-server's definition files.
Add a file with the line---@meta, followed by the variables defined by thesnip_envto any directory listed in theworkspace.library-settings forlua-langue-server(one likely directory isvim.fn.stdpath("config")/lua, check:checkhealth lspin a lua file to be sure).---@meta s = require("luasnip.nodes.snippet").S sn = require("luasnip.nodes.snippet").SN isn = require("luasnip.nodes.snippet").ISN t = require("luasnip.nodes.textNode").T i = require("luasnip.nodes.insertNode").I f = require("luasnip.nodes.functionNode").F c = require("luasnip.nodes.choiceNode").C d = require("luasnip.nodes.dynamicNode").D r = require("luasnip.nodes.restoreNode").R events = require("luasnip.util.events") k = require("luasnip.nodes.key_indexer").new_key ai = require("luasnip.nodes.absolute_indexer") extras = require("luasnip.extras") l = require("luasnip.extras").lambda rep = require("luasnip.extras").rep p = require("luasnip.extras").partial m = require("luasnip.extras").match n = require("luasnip.extras").nonempty dl = require("luasnip.extras").dynamic_lambda fmt = require("luasnip.extras.fmt").fmt fmta = require("luasnip.extras.fmt").fmta conds = require("luasnip.extras.expand_conditions") postfix = require("luasnip.extras.postfix").postfix types = require("luasnip.util.types") parse = require("luasnip.util.parser").parse_snippet ms = require("luasnip.nodes.multiSnippet").new_multisnippet
While that allows the
snip_env-variables to resolve correctly in snippet-files, it also resolves them in other lua files. This can be fixed by putting the file in a directory that is not inworkspace.library(create, for example,vim.fn.stdpath("state")/luasnip-snip_env/), and then adding its path toworkspace.libraryonly for snippet files, for example by putting a.luarc.jsonwith the following content into all snippet-directories:{ "workspace.library": ["<state-stdpath>/luasnip-snip_env"] }
While the lua-snippet-files will be reloaded on edit, this does not
automatically happen if a file the snippet-file depends on (e.g. via require)
is changed.
Since this still may still be desirable, there are two functions exposed when a
file is loaded by the lua-loader: ls_tracked_dofile and
ls_tracked_dopackage. They perform like dofile and (almost like) require,
but both register the loaded file internally as a dependency of the
snippet-file, so it can be reloaded when the loaded file is edited. As stated,
ls_tracked_dofile behaves exactly like dofile, but does the dependency-work
as well.
ls_tracked_dopackage mimics require in that it does not take a path, but a
module-name like "luasnip.loaders.from_lua", and then searches the
runtimepath/lua-directories, and path and cpath for the module.
Unlike require, the file will not be cached, since that would complicate the
reload-on-edit-behavior.
To easily edit snippets for the current session, the files loaded by any loader
can be quickly edited via
require("luasnip.loaders").edit_snippet_files(opts:table|nil)
When called, it will open a vim.ui.select-dialog to select first a filetype,
and then (if there are multiple) the associated file to edit.
opts contains four settings:
-
ft_filter:fn(filetype:string) -> boolOptionally filter initially listed filetypes.true-> filetype will be listed,false-> not listed. Accepts all filetypes by default. -
format:fn(file:string, source_name:string) -> string|nil
fileis simply the path to the file,source_nameis one of"lua","snipmate"or"vscode".
If a string is returned, it is used as the title of the item,nilon the other hand will filter out this item.
The default simply replaces some long strings (packer-path and config-path) infilewith shorter, symbolic names ("$PLUGINS","$CONFIG"), but this can be extended to-
filter files from some specific source/path
-
more aggressively shorten paths using symbolic names, e.g.
"$FRIENDLY_SNIPPETS".
Example: hide the*.luasnippet files, and shorten the path with$LuaSnip:require "luasnip.loaders" .edit_snippet_files { format = function(file, source_name) if source_name == "lua" then return nil else return file:gsub("/root/.config/nvim/luasnippets", "$LuaSnip") end end }
-
-
edit:fn(file:string)This function is supposed to open the file for editing. The default is a simplevim.cmd("edit " .. file)(replace the current buffer), but one could open the file in a split, a tab, or a floating window, for example. -
extend:fn(ft:string, ft_paths:string[]) -> (string,string)[]
This function can be used to create additional choices for the file-selection.ft: The filetype snippet-files are queried for.ft_paths: list of paths to the known snippet files.
The function should return a list of
(string,string)-tuples. The first of each pair is the label that will appear in the selection-prompt, and the second is the path that will be passed to theedit()function if that item was selected.This can be used to create a new snippet file for the current filetype:
require("luasnip.loaders").edit_snippet_files {
extend = function(ft, paths)
if #paths == 0 then
return {
{ "$CONFIG/" .. ft .. ".snippets",
string.format("%s/%s.snippets", <PERSONAL_SNIPPETS_FOLDER>, ft) }
}
end
return {}
end
}One comfortable way to call this function is registering it as a command:
command! LuaSnipEdit :lua require("luasnip.loaders").edit_snippet_files()SnippetProxy is used internally to alleviate the upfront cost of
loading snippets from e.g. a SnipMate library or a VSCode package. This is
achieved by only parsing the snippet on expansion, not immediately after reading
it from some file.
SnippetProxy may also be used from Lua directly to get the same benefits:
This will parse the snippet on startup:
ls.parser.parse_snippet("trig", "a snippet $1!")while this will parse the snippet upon expansion:
local sp = require("luasnip.nodes.snippetProxy")
sp("trig", "a snippet $1")sp(context, body, opts) -> snippetProxy
context: exactly the same as the first argument passed tols.s.body: the snippet body.opts: accepts the sameoptsasls.s, with some additions:parse_fn: the function for parsing the snippet. Defaults tols.parser.parse_snippet(the parser for LSP snippets), an alternative is the parser for SnipMate snippets (ls.parser.parse_snipmate).
ext_opts can be used to set the opts (see nvim_buf_set_extmark) of the
extmarks used for marking node positions, either globally, per snippet or
per node.
This means that they allow highlighting the text inside of nodes, or adding
virtual text to the line the node begins on.
This is an example for the node_ext_opts used to set ext_opts of single nodes:
local ext_opts = {
-- these ext_opts are applied when the node is active (e.g. it has been
-- jumped into, and not out yet).
active =
-- this is the table actually passed to `nvim_buf_set_extmark`.
{
-- highlight the text inside the node red.
hl_group = "GruvboxRed"
},
-- these ext_opts are applied when the node is not active, but
-- the snippet still is.
passive = {
-- add virtual text on the line of the node, behind all text.
virt_text = {{"virtual text!!", "GruvboxBlue"}}
},
-- visited or unvisited are applied when a node was/was not jumped into.
visited = {
hl_group = "GruvboxBlue"
},
unvisited = {
hl_group = "GruvboxGreen"
},
-- and these are applied when both the node and the snippet are inactive.
snippet_passive = {}
}
s("trig", {
i(1, "text1", {
node_ext_opts = ext_opts
}),
i(2, "text2", {
node_ext_opts = ext_opts
})
})In the above example, the text inside the insertNodes is highlighted in green if
they were not yet visited, in blue once they were, and red while they are.
The virtual text "virtual text!!" is visible as long as the snippet is active.
To make defining ext_opts less verbose, more specific states inherit from less
specific ones:
passiveinherits fromsnippet_passivevisitedandunvisitedfrompassiveactivefromvisited
flowchart TD
visited --> active
passive --> visited
passive --> unvisited
snippet_passive --> passive
To disable a key from a less specific state, it has to be explicitly set to its
default, e.g. to disable highlighting inherited from passive when the node is
active, hl_group should be set to None.
As stated earlier, these ext_opts can also be applied globally or for an
entire snippet. For this, it's necessary to specify which kind of node a given
set of ext_opts should be applied to:
local types = require("luasnip.util.types")
ls.setup({
ext_opts = {
[types.insertNode] = {
active = {...},
visited = {...},
passive = {...},
snippet_passive = {...}
},
[types.choiceNode] = {
active = {...},
unvisited = {...}
},
[types.snippet] = {
passive = {...}
}
}
})The above applies the given ext_opts to all nodes of these types, in all
snippets.
local types = require("luasnip.util.types")
s("trig", { i(1, "text1"), i(2, "text2") }, {
child_ext_opts = {
[types.insertNode] = {
passive = {
hl_group = "GruvboxAqua"
}
}
}
})However, the ext_opts here are only applied to the insertNodes inside this
snippet.
By default, the ext_opts actually used for a node are created by extending the
node_ext_opts with the effective_child_ext_opts[node.type] of the parent,
which are in turn the parent's child_ext_opts extended with the global
ext_opts (those set ls.setup).
It's possible to prevent both of these merges by passing
merge_node/child_ext_opts=false to the snippet/node-opts:
ls.setup({
ext_opts = {
[types.insertNode] = {
active = {...}
}
}
})
s("trig", {
i(1, "text1", {
node_ext_opts = {
active = {...}
},
merge_node_ext_opts = false
}),
i(2, "text2")
}, {
child_ext_opts = {
[types.insertNode] = {
passive = {...}
}
},
merge_child_ext_opts = false
})The hl_group of the global ext_opts can also be set via standard
highlight groups:
vim.cmd("hi link LuasnipInsertNodePassive GruvboxRed")
vim.cmd("hi link LuasnipSnippetPassive GruvboxBlue")
-- needs to be called for resolving the effective ext_opts.
ls.setup({})The names for the used highlight groups are
"Luasnip<node>{Passive,Active,SnippetPassive}", where <node> can be any kind of
node in PascalCase (or "Snippet").
One problem that might arise when nested nodes are highlighted is that the highlight of inner nodes should be visible, e.g. above that of nodes they are nested inside.
This can be controlled using the priority-key in ext_opts. In
nvim_buf_set_extmark, that value is an absolute value, but here it is relative
to some base-priority, which is increased for each nesting level of
snippet(Nodes)s.
Both the initial base-priority and its' increase and can be controlled using
ext_base_prio and ext_prio_increase:
ls.setup({
ext_opts = {
[types.insertNode] = {
active = {
hl_group = "GruvboxBlue",
-- the priorities should be \in [0, ext_prio_increase).
priority = 1
}
},
[types.choiceNode] = {
active = {
hl_group = "GruvboxRed"
-- priority defaults to 0
}
}
}
ext_base_prio = 200,
ext_prio_increase = 2
})Here the highlight of an insertNode nested directly inside a choiceNode is
always visible on top of it.
Snippet docstrings can be queried using snippet:get_docstring(). The function
evaluates the snippet as if it was expanded regularly, which can be problematic
if e.g. a dynamicNode in the snippet relies on inputs other than
the argument nodes.
snip.env and snip.captures are populated with the names of the queried
variable and the index of the capture respectively
(snip.env.TM_SELECTED_TEXT -> '$TM_SELECTED_TEXT', snip.captures[1] ->
'$CAPTURES1'). Although this leads to more expressive docstrings, it can
cause errors in functions that e.g. rely on a capture being a number:
s({trig = "(%d)", regTrig = true}, {
f(function(args, snip)
return string.rep("repeatme ", tonumber(snip.captures[1]))
end, {})
})This snippet works fine because snippet.captures[1] is always a number.
During docstring generation, however, snippet.captures[1] is '$CAPTURES1',
which will cause an error in the functionNode.
Issues with snippet.captures can be prevented by specifying docTrig during
snippet-definition:
s({trig = "(%d)", regTrig = true, docTrig = "3"}, {
f(function(args, snip)
return string.rep("repeatme ", tonumber(snip.captures[1]))
end, {})
})snippet.captures and snippet.trigger will be populated as if actually
triggered with 3.
Other issues will have to be handled manually by checking the contents of e.g.
snip.env or predefining the docstring for the snippet:
s({trig = "(%d)", regTrig = true, docstring = "repeatmerepeatmerepeatme"}, {
f(function(args, snip)
return string.rep("repeatme ", tonumber(snip.captures[1]))
end, {})
})Refer to #515 for a
better example to understand docTrig and docstring.
Although generation of docstrings is pretty fast, it's preferable to not
redo it as long as the snippets haven't changed. Using
ls.store_snippet_docstrings(snippets) and its counterpart
ls.load_snippet_docstrings(snippets), they may be serialized from or
deserialized into the snippets.
Both functions accept a table structured like this: {ft1={snippets}, ft2={snippets}}. Such a table containing all snippets can be obtained via
ls.get_snippets().
load should be called before any of the loader-functions as snippets loaded
from VSCode style packages already have their docstring set (docstrings
wouldn't be overwritten, but there'd be unnecessary calls).
The cache is located at stdpath("cache")/luasnip/docstrings.json (probably
~/.cache/nvim/luasnip/docstrings.json).
Events can be used to react to some action inside snippets. These callbacks can
be defined per snippet (callbacks-key in snippet constructor), per-node by
passing them as node_callbacks in node_opts, or globally (autocommand).
callbacks: fn(node[, event_args]) -> event_res
All callbacks receive the node associated with the event and event-specific
optional arguments, event_args.
event_res is only used in one event, pre_expand, where some properties of
the snippet can be changed. If multiple callbacks return event_res, we only
guarantee that one of them will be effective, not all of them.
autocommand:
Luasnip uses User-events. Autocommands for these can be registered using
au User SomeUserEvent echom "SomeUserEvent was triggered"or
vim.api.nvim_create_autocommand("User", {
pattern = "SomeUserEvent",
command = "echom SomeUserEvent was triggered"
})The node and event_args can be accessed through require("luasnip").session:
node:session.event_nodeevent_args:session.event_args
Events:
enter/leave: Called when a node is entered/left (for example when jumping around in a snippet).
User-event:"Luasnip<Node>{Enter,Leave}", with<Node>in PascalCase, e.g.InsertNodeorDynamicNode.
event_args: nonechange_choice: When the active choice in achoiceNodeis changed.
User-event:"LuasnipChangeChoice"
event_args: nonepre_expand: Called before a snippet is expanded. Modifying text is allowed, the expand-position will be adjusted so the snippet expands at the same position relative to existing text.
User-event:"LuasnipPreExpand"
event_args:expand_pos:{<row>, <column>}, position at which the snippet will be expanded.<row>and<column>are both 0-indexed.expand_pos_mark_id:number, the id of the extmark LuaSnip uses to trackexpand_pos. This may be moved around freely.event_res:env_override:map string->(string[]|string), override or extend the snippet's environment (snip.env).
A pretty useless, beyond serving as an example here, application of these would be printing e.g. the node's text after entering:
vim.api.nvim_create_autocmd("User", {
pattern = "LuasnipInsertNodeEnter",
callback = function()
local node = require("luasnip").session.event_node
print(table.concat(node:get_text(), "\n"))
end
})or some information about expansions:
vim.api.nvim_create_autocmd("User", {
pattern = "LuasnipPreExpand",
callback = function()
-- get event-parameters from `session`.
local snippet = require("luasnip").session.event_node
local expand_position =
require("luasnip").session.event_args.expand_pos
print(string.format("expanding snippet %s at %s:%s",
table.concat(snippet:get_docstring(), "\n"),
expand_position[1],
expand_position[2]
))
end
})The function ls.cleanup() triggers the LuasnipCleanup user event, that you
can listen to do some kind of cleaning in your own snippets; by default it will
empty the snippets table and the caches of the lazy_load.
Luasnip uses logging to report unexpected program states, and information on what's going on in general. If something does not work as expected, taking a look at the log (and potentially increasing the log level) might give some good hints towards what is going wrong.
The log is stored in <vim.fn.stdpath("log")>/luasnip.log
(<vim.fn.stdpath("cache")>/luasnip.log for Neovim versions where
stdpath("log") does not exist), and can be opened by calling ls.log.open(). You can get the log path through ls.log.log_location().
The log level (granularity of reported events) can be adjusted by calling
ls.log.set_loglevel("error"|"warn"|"info"|"debug"). "debug" has the highest
granularity, "error" the lowest, the default is "warn".
You can also adjust the datetime formatting through the ls.log.time_fmt variable. By default, it uses the '%X' formatting, which results in the full time (hour, minutes and seconds) being shown.
Once this log grows too large (10MiB, currently not adjustable), it will be
renamed to luasnip.log.old, and a new, empty log created in its place. If
there already exists a luasnip.log.old, it will be deleted.
ls.log.ping() can be used to verify the log is working correctly: it will
print a short message to the log.
It is possible to attach, to a snippet, information about its source. This can
be done either by the various loaders (if it is enabled in ls.setup
(Config-Options, loaders_store_source)), or manually. The
attached data can be used by Extras-Snippet-Location to
jump to the definition of a snippet.
It is also possible to get/set the source of a snippet via API:
ls.snippet_source:
get(snippet) -> source_data: Retrieve the source-data ofsnippet.source_dataalways contains the keyfile, the file in which the snippet was defined, and may additionally containlineorline_end, the first and last line of the definition.set(snippet, source): Set the source of a snippet.snippet: a snippet which was added vials.add_snippets.source: asource-object, obtained from eitherfrom_debuginfoorfrom_location.
from_location(file, opts) -> source:file:string, The path to the file in which the snippet is defined.opts:table|nil, optional parameters for the source.line:number, the first line of the definition. 1-indexed.line_end:number, the final line of the definition. 1-indexed.
from_debuginfo(debuginfo) -> source: Generates source from the table returned bydebug.getinfo(from now on referred to asdebuginfo).debuginfohas to be of a frame of a function which is backed by a file, and has to contain this information, i.e. has to be generated bydebug.get_info(*, "Sl")(at least"Sl", it may also contain more info).
Many snippets use the $TM_SELECTED_TEXT or (for LuaSnip, preferably
LS_SELECT_RAW or LS_SELECT_DEDENT) variable, which has to be populated by
selecting and then yanking (and usually also cutting) text from the buffer
before expanding.
By default, this is disabled (as to not pollute keybindings which may be used for something else), so one has to
- either set
cut_selection_keysinsetup(see Config-Options). - or map
ls.cut_keysas the right-hand-side of a mapping - or manually configure the keybinding. For this, create a new keybinding that
<Esc>es to NORMAL (to populate the<and>-markers)- calls
luasnip.pre_yank(<namedreg>) - yanks text to some named register
<namedreg> - calls
luasnip.post_yank(<namedreg>)Take care that the yanking actually takes place between the two calls. One way to ensure this is to call the two functions via<cmd>lua ...<Cr>:
The reason for this specific order is to allow us to take a snapshot of registers (in the pre-callback), and then restore them (in the post-callback) (so that we may get the visual selection directly from the register, which seems to be the most foolproof way of doing this).vim.keymap.set("v", "<Tab>", [[<Esc><cmd>lua require("luasnip.util.select").pre_yank("z")<Cr>gv"zs<cmd>lua require('luasnip.util.select').post_yank("z")<Cr>]])
These are the settings you can provide to luasnip.setup():
-
keep_roots: Whether snippet-roots should be linked. See Basics-Snippet-Insertion for more context. -
link_roots: Whether snippet-roots should be linked. See Basics-Snippet-Insertion for more context. -
exit_roots: Whether snippet-roots should exit at reaching at their last node,$0. This setting is only valid for root snippets, not child snippets. This setting may avoid unexpected behavior by disallowing to jump earlier (finished) snippets. Check Basics-Snippet-Insertion for more information on snippet-roots. -
link_children: Whether children should be linked. See Basics-Snippet-Insertion for more context. -
history(deprecated): if not nil,keep_roots,link_roots, andlink_childrenwill be set to the value ofhistory, andexit_rootswill set to inverse value ofhistory. This is just to ensure backwards-compatibility. -
update_events: Choose which events trigger an update of the active nodes' dependents. Default is just'InsertLeave','TextChanged,TextChangedI'would update on every change. These, like all other*_eventsare passed tonvim_create_autocmdasevents, so they can be wrapped in a table, likels.setup({ update_events = {"TextChanged", "TextChangedI"} })
-
region_check_events: Events on which to leave the current snippet-root if the cursor is outside its' 'region'. Disabled by default,'CursorMoved','CursorHold'or'InsertEnter'seem reasonable. -
delete_check_events: When to check if the current snippet was deleted, and if so, remove it from the history. Off by default,'TextChanged'(perhaps'InsertLeave', to react to changes done in Insert mode) should work just fine (alternatively, this can also be mapped using<Plug>luasnip-delete-check). -
cut_selection_keys: Mapping for populatingTM_SELECTED_TEXTand related variables (not set by default).
See Selection for more information. -
store_selection_keys(deprecated): same ascut_selection_keys -
enable_autosnippets: Autosnippets are disabled by default to minimize performance penalty if unused. Set totrueto enable. -
ext_opts: Additional options passed to extmarks. Can be used to add passive/active highlight on a per-node-basis (more info inDOC.md) -
parser_nested_assembler: Override the default behavior of inserting achoiceNodecontaining the nested snippet and an emptyinsertNodefor nested placeholders ("${1: ${2: this is nested}}"). For an example (behavior more similar to VSCode), check here -
ft_func: Source of possible filetypes for snippets. Defaults to a function, which returnsvim.split(vim.bo.filetype, ".", true), but check filetype_functions or the Extras-Filetype-Functions-section for more options. -
load_ft_func: Function to determine which filetypes belong to a given buffer (used forlazy_loading).fn(bufnr) -> filetypes (string[]). Again, there are some examples in filetype_functions. -
snip_env: The best way to author snippets in Lua involves thelua-loader(see Loaders-Lua). Unfortunately, this requires that snippets are defined in separate files, which means that common definitions likes,i,sn,t,fmt, ... have to be repeated in each of them, and that adding more customized functions to ease writing snippets also requires some setup.
snip_envcan be used to insert variables into exactly the places wherelua-snippetsare defined (for now only the file loaded by thelua-loader).
Settingsnip_envto{ some_global = "a value" }will add (amongst the defaults stated at the beginning of this documentation) the global variablesome_globalwhile evaluating these files.
There are special keys which, when set insnip_envchange the behavior of this option, and are not passed through to thelua-files:__snip_env_behaviour, string: either"set"or"extend"(default"extend")
If this is"extend", the variables defined insnip_envwill complement (and override) the defaults. If this is not desired,"set"will not include the defaults, but only the variables set here.
-
loaders_store_source, boolean, whether loaders should store the source of the loaded snippets.
Enabling this means that the definition of any snippet can be jumped to via Extras-Snippet-Location, but also entails slightly increased memory consumption (and load-time, but it's not really noticeable).
-
Filetypes. LuaSnip uses
allas the global filetype. As most snippet collections don't explicitly target LuaSnip, they may not provide global snippets for this filetype, but another, like_(honza/vim-snippets). In these cases, it's necessary to extend LuaSnip's global filetype with the collection's global filetype:ls.filetype_extend("all", { "_" })
In general, if some snippets don't show up when loading a collection, a good first step is checking the filetype LuaSnip is actually looking into (print them for the current buffer via
:lua print(vim.inspect(require("luasnip").get_snippet_filetypes()))), against the one the missing snippet is provided for (in the collection).
If there is indeed a mismatch,filetype_extendcan be used to also search the collection's filetype:ls.filetype_extend("<luasnip-filetype>", { "<collection-filetype>" })
-
Non-default
ft_funcloading. As we only loadlazy_loaded snippets on some events,lazy_loadwill probably not play nice when a non-defaultft_funcis used: if it depends on e.g. the cursor position, only the filetypes for the cursor position when thelazy_loadevents are triggered will be loaded. Check Extras-Filetype-Function'sextend_load_ftfor a solution.
-
Snippets sharing triggers. If multiple snippets could be triggered at the current buffer-position, the snippet that was defined first in one's configuration will be expanded first. As a small, real-world LaTeX math example, given the following two snippets with triggers
.ovandov:postfix( -- Insert over-line command to text via post-fix { trig = ".ov", snippetType = "autosnippet" }, { f(function(_, parent) return "\\overline{" .. parent.snippet.env.POSTFIX_MATCH .. "}" end, {}), } ), s( -- Insert over-line command { trig = "ov", snippetType="autosnippet" }, fmt( [[\overline{<>}]], { i(1) }, { delimiters = "<>" } ) ),
If one types
xfollowed by.ov, the postfix snippet expands producing\overline{x}. However, if thepostfixsnippet above is defined after the normal snippets, then the same key press sequence producesx.\overline{}. This behavior can be overridden by explicitly providing a priority to such snippets. For example, in the above code, if thepostfixsnippet was defined after the normal snippets, then addingpriority=1001to thepostfixsnippet will cause it to expand as if it were defined before the normal snippets. Snippetpriorityis discussed in the Snippets section of the documentation.
require("luasnip"):
Get the currently active snippet.
This function returns:
LuaSnip.Snippet?The active snippet if one exists, ornil.
Retrieve snippets from luasnip.
-
ft?: string?Filetype, if not given returns snippets for all filetypes. -
opts?: LuaSnip.Opts.GetSnippets?Optional arguments.
Valid keys are:type?: ("snippets"|"autosnippets")?Whether to get snippets or autosnippets. Defaults to "snippets".
This function returns:
(LuaSnip.Snippet[]|{ [string]: LuaSnip.Snippet[] })Flat array whenftis non-nil, otherwise a table mapping filetypes to snippets.
Retrieve information about snippets available in the current file/at the current position (in case treesitter-based filetypes are enabled).
snip_info?: fun(LuaSnip.Snippet) -> T?Optionally pass a function that, given a snippet, returns the data that is returned by this function in the snippets' stead. By default, this function isfunction(snip) return { name = snip.name, trigger = snip.trigger, description = snip.description, wordTrig = snip.wordTrig and true or false, regTrig = snip.regTrig and true or false, } end
This function returns:
{ [string]: T[] }Table mapping filetypes to list of data returned by snip_info function.
Removes the current snippet from the jumplist (useful if LuaSnip fails to automatically detect e.g. deletion of a snippet) and sets the current node behind the snippet, or, if not possible, before it.
Jump forwards or backwards
dir: (1|-1)Jump forward for 1, backward for -1.
This function returns:
booleantrueif a jump was performed,falseotherwise.
Find the node the next jump will end up at. This will not work always, because we will not update the node before jumping, so if the jump would e.g. insert a new node between this node and its pre-update jump target, this would not be registered. Thus, it currently only works for simple cases.
dir: (1|-1)1: find the next node,-1: find the previous node.
This function returns:
LuaSnip.NodeThe destination node.
Return whether jumping forwards or backwards will actually jump, or if there is no node in that direction.
dir: (1|-1)1forward,-1backward.
Return whether there is an expandable snippet at the current cursor position. Does not consider autosnippets since those would already be expanded at this point.
Return whether it's possible to expand a snippet at the current cursor-position, or whether it's possible to jump forward from the current node.
Determine whether the cursor is within a snippet.
Return whether a snippet can be expanded at the current cursor position, or whether the cursor is inside a snippet and the current node can be jumped forward from.
Return whether the cursor is inside a snippet and the current node can be jumped forward from.
dir: (1|-1)Test jumping forwards/backwards.
Expand a snippet in the current buffer.
-
snippet: LuaSnip.SnippetThe snippet. -
opts?: LuaSnip.Opts.SnipExpand?Optional additional arguments.
Valid keys are:-
clear_region?: LuaSnip.BufferRegion?A region of text to clear after populating env-variables, but before jumping intosnip. Ifnil, no clearing is performed. Being able to remove text at this point is useful as clearing before calling this function would populateTM_CURRENT_LINEandTM_CURRENT_WORDwith wrong values (they would miss the snippet trigger). The actual values used for clearing areregion.fromandregion.to, both (0,0)-indexed byte-positions in the buffer. -
expand_params?: LuaSnip.Opts.SnipExpandExpandParams?Override various fields of the expanded snippet. Don't override anything by default. This is useful for manually expanding snippets where the trigger passed viatrigis not the text triggering the snippet, or those which expectcaptures(basically, snippets with a non-plaintexttrigEngine).One Example:
snip_expand(snip, { trigger = "override_trigger", captures = {"first capture", "second capture"}, env_override = { this_key = "some value", other_key = {"multiple", "lines"}, TM_FILENAME = "some_other_filename.lua" } })
Valid keys are:
trigger?: string?What to set as the expanded snippets' trigger (Defaults tosnip.trigger).captures?: string[]?Set as the expanded snippets' captures (Defaults to{}).env_override?: { [string]: string }?Set or override environment variables of the expanded snippet (Defaults to{}).
-
pos?: (integer,integer)?Position at which the snippet should be inserted. Pass as(row,col), both 0-based, thecolgiven in bytes. -
indent?: boolean?Whether to prepend the current lines' indent to all lines of the snippet. (Defaults totrue) Turning this off is a good idea when a LSP server already takes indents into consideration. In such cases, LuaSnip should not add additional indents. If you are usingnvim-cmp, this could be used as follows:require("cmp").setup { snippet = { expand = function(args) local indent_nodes = true if vim.api.nvim_get_option_value("filetype", { buf = 0 }) == "dart" then indent_nodes = false end require("luasnip").lsp_expand(args.body, { indent = indent_nodes, }) end, }, }
-
jump_into_func?: fun(snip: LuaSnip.Snippet) -> LuaSnip.Node?
-
This function returns:
LuaSnip.ExpandedSnippetThe snippet that was inserted into the buffer.
Find a snippet whose trigger matches the text before the cursor and expand it.
-
opts?: LuaSnip.Opts.Expand?Subset of opts accepted bysnip_expand.
Valid keys are:jump_into_func?: fun(snip: LuaSnip.Snippet) -> LuaSnip.Node?
This function returns:
booleanWhether a snippet was expanded.
Find an autosnippet matching the text at the cursor-position and expand it.
Repeat the last performed snip_expand. Useful for dot-repeat.
Expand at the cursor, or jump forward.
This function returns:
booleanWhether an action was performed.
Expand a snippet specified in lsp-style.
body: stringA string specifying a lsp-snippet, e.g."[${1:text}](${2:url})"opts?: LuaSnip.Opts.SnipExpand?Optional args passed through tosnip_expand.
Return whether the current node is inside a choiceNode.
Change the currently active choice.
val: (1|-1)Move one choice forward or backward.
Set the currently active choice.
choice_indx: integerIndex of the choice to switch to.
Get a string-representation of all the current choiceNode's choices.
This function returns:
string[]\n-concatenated lines of every choice.
Update all nodes that depend on the currently-active node.
Generate and store the docstrings for a list of snippets as generated by get_snippets(). The
docstrings are stored at stdpath("cache") .. "/luasnip/docstrings.json", are indexed by their
trigger, and should be updated once any snippet changes.
snippet_table: { [string]: LuaSnip.Snippet[] }A table mapping some keys to lists of snippets (keys are most likely filetypes).
Provide all passed snippets with a previously-stored (via store_snippet_docstrings) docstring.
This prevents a somewhat costly computation which is performed whenever a snippets' docstring is
first retrieved, but may cause larger delays when snippet_table contains many of snippets. Utilize
this function by calling ls.store_snippet_docstrings(ls.get_snippets()) whenever snippets are
modified, and ls.load_snippet_docstrings(ls.get_snippets()) on startup.
snippet_table: { [string]: LuaSnip.Snippet[] }List of snippets, should contain the same keys (filetypes) as the table that was passed tostore_snippet_docstrings. Again, most likely the result ofget_snippets.
Checks whether (part of) the current snippet's text was deleted, and removes it from the jumplist if it was (it cannot be jumped back into).
Checks whether the cursor is still within the range of the root-snippet node belongs to. If yes,
no change occurs; if no, the root-snippet is exited and its i(0) will be the new active node. If a
jump causes an error (happens mostly because the text of a snippet was deleted), the snippet is
removed from the jumplist and the current node set to the end/beginning of the next/previous
snippet.
node: LuaSnip.Node
Make all snippets that belong to one of the filetypes in extend_fts available to the filetype
ft.
Example:
ls.filetype_extend("sh", {"zsh"})
ls.filetype_extend("sh", {"bash"})This makes all zsh- and bash-snippets available in sh-buffers.
ft: stringextend_fts: string[]
Make all snippets that belong to one of the filetypes in extend_fts available to the filetype ft
and remove all previous extension-filetypes added by filetype_extend or filetype_set.
ft: stringextend_fts: string[]
Clear all loaded snippets. Also sends the User LuasnipCleanup autocommand, so plugins that depend
on luasnip's snippet-state can clean up their now-outdated state.
Trigger the User LuasnipSnippetsAdded autocommand that signifies to other plugins that a filetype
has received new snippets.
ft: stringThe filetype that has new snippets. Code that listens to this event can retrieve this filetype fromrequire("luasnip").session.latest_load_ft.
Injects the fields defined in snip_env, in setup, into the callers global environment.
This means that variables like s, sn, i, t, ... (by default) work, and are useful for
quickly testing snippets in a buffer:
local ls = require("luasnip")
ls.setup_snip_env()
ls.add_snippets("all", {
s("choicetest", {
t":", c(1, {
t("asdf", {node_ext_opts = {active = { virt_text = {{"asdf", "Comment"}} }}}),
t("qwer", {node_ext_opts = {active = { virt_text = {{"qwer", "Comment"}} }}}),
})
})
}, { key = "3d9cd211-c8df-4270-915e-bf48a0be8a79" })where the key makes it easy to reload the snippets on changes, since the previously registered
snippets will be replaced when the buffer is re-sourced.
Return the currently active snip_env.
Get the snippet corresponding to some id.
id: LuaSnip.SnippetID
Add snippets to luasnip's snippet-collection.
NOTE: Calls refresh_notify as needed if enabled via opts.refresh_notify.
ft?: string?The filetype to add the snippets to, or nil if the filetype is specified insnippets.snippets: (LuaSnip.Addable[]|{ [string]: LuaSnip.Addable[] })Ifftis nil a table mapping a filetype to a list of snippets, otherwise a flat table of snippets.LuaSnip.Addableare objects created by e.g. the functionss,ms, orsp.opts?: LuaSnip.Opts.AddSnippets?Optional arguments.
Clean invalidated snippets from internal snippet storage. Invalidated snippets are still stored; it might be useful to actually remove them as they still have to be iterated during expansion.
-
opts?: LuaSnip.Opts.CleanInvalidated?Additional, optional arguments.
Valid keys are:inv_limit?: integer?If set, invalidated snippets are only cleared if their number exceedsinv_limit.
Lookup a node by position and activate (ie. jump into) it.
-
opts?: LuaSnip.Opts.ActivateNode?Additional, optional arguments.
Valid keys are:strict?: boolean?Only activate nodes one could usually jump to. (Defaults to false)select?: boolean?Whether to select the entire node, or leave the cursor at the position it is currently at. (Defaults to true)pos?: LuaSnip.BytecolBufferPosition?Where to look for the node. (Defaults to the position of the cursor)
Not covered in this section are the various node-constructors exposed by
the module, their usage is shown either previously in this file or in
Examples/snippets.lua (in the repository).












































{ // a comment, since `.code-snippets` may contain jsonc. "c/cpp-snippet": { "prefix": [ "trigger1", "trigger2" ], "body": [ "this is $1", "my snippet $2" ], "description": "A description of the snippet.", "scope": "c,cpp" }, "python-snippet": { "prefix": "trig", "body": [ "this is $1", "a different snippet $2" ], "description": "Another snippet-description.", "scope": "python" }, "global snippet": { "prefix": "trigg", "body": [ "this is $1", "the last snippet $2" ], "description": "One last snippet-description.", } }