-
-
Notifications
You must be signed in to change notification settings - Fork 245
Cool Snippets
Got a cool or useful snippet that you'd like to show off share with the community?
Do it here! Make sure to include a short description to find snippets for a specific filetype. Other users will also surely appreciate a short explaination of that weird pattern or the 100-line dynamicNode you used ;)
Of course, this is just a recommendation :)
local rec_ls
rec_ls = function()
return sn(nil, {
c(1, {
-- important!! Having the sn(...) as the first choice will cause infinite recursion.
t({""}),
-- The same dynamicNode as in the snippet (also note: self reference).
sn(nil, {t({"", "\t\\item "}), i(1), d(2, rec_ls, {})}),
}),
});
end
...
s("ls", {
t({"\\begin{itemize}",
"\t\\item "}), i(1), d(2, rec_ls, {}),
t({"", "\\end{itemize}"}), i(0)
})
This snippet first expands to
\begin{itemize}
\item $1
\end{itemize}
upon jumping into the dynamicNode
and changing the choice, another dynamicNode
(and with it, \item
) will be inserted.
As the dynamicNode
is recursive, it's possible to insert as many \item
s' as desired.
(The gif also showcases Luasnips ability to preserve input to collapsed choices!)
local tex = {}
tex.in_mathzone = function()
return vim.fn['vimtex#syntax#in_mathzone']() == 1
end
tex.in_text = function()
return not tex.in_mathzone()
end
...
s("dm", {
t({ "\\[", "\t" }),
i(1),
t({ "", "\\]" }),
}, { condition = tex.in_text })
This snippet will only expand in text so that you may type dm
safely in math environments.
Moreover, using the following condition,
the snippet will only be expanded in beamer
documents.
tex.in_beamer = function()
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
for _, line in ipairs(lines) do
if line:match("\\documentclass{beamer}") then
return true
end
end
return false
end
-- or, use `vimtex` to detect
-- tex.in_beamer = function()
-- return vim.b.vimtex["documentclass"] == "beamer"
-- end
...
s("bfr", {
t({ "\\begin{frame}", "\t\\frametitle{" }),
i(1, "frame title"),
t({ "}", "\t" }),
i(0),
t({ "", "\\end{frame}" }),
}, { condition = tex.in_beamer })
Indeed, you may combine some of the conditions to build your own context-aware snippets.
With this snippet you can dynamically generate a table. It contain two main function. The first function return a snipnode which includes an insert node for each column defined by the user.
table_node= function(args)
local tabs = {}
local count
table = args[1][1]:gsub("%s",""):gsub("|","")
count = table:len()
for j=1, count do
local iNode
iNode = i(j)
tabs[2*j-1] = iNode
if j~=count then
tabs[2*j] = t" & "
end
end
return sn(nil, tabs)
end
The second one it is based on the rec_ls
function from endless list entry.
rec_table = function ()
return sn(nil, {
c(1, {
t({""}),
sn(nil, {t{"\\\\",""} ,d(1,table_node, {ai[1]}), d(2, rec_table, {ai[1]})})
}),
});
end
Based on those functions, the snips will be
s("table", {
t"\\begin{tabular}{",
i(1,"0"),
t{"}",""},
d(2, table_node, {1}, {}),
d(3, rec_table, {1}),
t{"","\\end{tabular}"}
}),
local function char_count_same(c1, c2)
local line = vim.api.nvim_get_current_line()
-- '%'-escape chars to force explicit match (gsub accepts patterns).
-- second return value is number of substitutions.
local _, ct1 = string.gsub(line, '%'..c1, '')
local _, ct2 = string.gsub(line, '%'..c2, '')
return ct1 == ct2
end
local function even_count(c)
local line = vim.api.nvim_get_current_line()
local _, ct = string.gsub(line, c, '')
return ct % 2 == 0
end
local function neg(fn, ...)
return not fn(...)
end
local function part(fn, ...)
local args = {...}
return function() return fn(unpack(args)) end
end
-- This makes creation of pair-type snippets easier.
local function pair(pair_begin, pair_end, expand_func, ...)
-- triggerd by opening part of pair, wordTrig=false to trigger anywhere.
-- ... is used to pass any args following the expand_func to it.
return s({trig = pair_begin, wordTrig=false},{
t({pair_begin}), i(1), t({pair_end})
}, {
condition = part(expand_func, part(..., pair_begin, pair_end))
})
end
...
-- these should be inside your snippet-table.
pair("(", ")", neg, char_count_same),
pair("{", "}", neg, char_count_same),
pair("[", "]", neg, char_count_same),
pair("<", ">", neg, char_count_same),
pair("'", "'", neg, even_count),
pair('"', '"', neg, even_count),
pair("`", "`", neg, even_count),
These snippets make use of the expand-condition to only trigger if a closing end of the pair is missing (lots of false positives and negatives, eg. if the closing part from a paren higher up in the text is on the current line, but it works more oft than not).
Another nice thing is using a function to create similar snippets.
s("mk", {
t("$"), i(1), t("$")
},{
callbacks = {
-- index `-1` means the callback is on the snippet as a whole
[-1] = {
[events.leave] = function ()
vim.cmd([[
autocmd InsertCharPre <buffer> ++once lua _G.if_char_insert_space()
]])
end
}
}
}),
_G.if_char_insert_space = function ()
if string.find(vim.v.char, "%a") then
vim.v.char = " " .. vim.v.char
end
end
This snippet has the following behavior after expansion: |
represents the cursor
-
$|$
jump ->$$|
insert any letter ->$$ a
-
$$|
insert anything else ->$$.
-
see :h InsertCharPre
if you want to understand how it works.
The following snippet expands to a function definition in Python and has choiceNode
s for if a return declaration should be used and what doc-string to use:
- None.
- Single line.
- Multiline with
Args
/Returns
.
The choiceNode
of the doc-string is manipulated to show different virtual text for the different choices to make it easy to now which one your in. This way of doing it is a sort of prototype and will be easier in the future. See discussion on #291.
The full snippet can be improved a lot to for example add the correct arguments from the function to the mulitline doc-string automatically.
local ls = require('luasnip')
local s = ls.snippet
local i = ls.insert_node
local t = ls.text_node
local c = ls.choice_node
local sn = ls.snippet_node
local isn = ls.indent_snippet_node
local fmt = require('luasnip.extras.fmt').fmt
local types = require("luasnip.util.types")
local function node_with_virtual_text(pos, node, text)
local nodes
if node.type == types.textNode then
node.pos = 2
nodes = {i(1), node}
else
node.pos = 1
nodes = {node}
end
return sn(pos, nodes, {
node_ext_opts = {
active = {
-- override highlight here ("GruvboxOrange").
virt_text = {{text, "GruvboxOrange"}}
}
}
})
end
local function nodes_with_virtual_text(nodes, opts)
if opts == nil then
opts = {}
end
local new_nodes = {}
for pos, node in ipairs(nodes) do
if opts.texts[pos] ~= nil then
node = node_with_virtual_text(pos, node, opts.texts[pos])
end
table.insert(new_nodes, node)
end
return new_nodes
end
local function choice_text_node(pos, choices, opts)
choices = nodes_with_virtual_text(choices, opts)
return c(pos, choices, opts)
end
local ct = choice_text_node
ls.add_snippets("python", {
s('d', fmt([[
def {func}({args}){ret}:
{doc}{body}
]], {
func = i(1),
args = i(2),
ret = c(3, {
t(''),
sn(nil, {
t(' -> '),
i(1),
}),
}),
doc = isn(4, {ct(1, {
t(''),
-- NOTE we need to surround the `fmt` with `sn` to make this work
sn(1, fmt([[
"""{desc}"""
]], {desc = i(1)})),
sn(2, fmt([[
"""{desc}
Args:
{args}
Returns:
{returns}
"""
]], {
desc = i(1),
args = i(2), -- TODO should read from the args in the function
returns = i(3),
})),
}, {
texts = {
"(no docstring)",
"(single line docstring)",
"(full docstring)",
}
})}, "$PARENT_INDENT\t"),
body = i(0),
}))
})
This is just the solution I decided to use. With help of @L3MON4D3 we developed multiple solutions see this issue including one using a similar functionality like the latex tabular macro (see dynamicnode with user input).
local ls = require"luasnip"
local s = ls.snippet
local sn = ls.snippet_node
local t = ls.text_node
local i = ls.insert_node
local c = ls.choice_node
local d = ls.dynamic_node
local r = ls.restore_node
local fmt = require("luasnip.extras.fmt").fmt
-- see latex infinite list for the idea. Allows to keep adding arguments via choice nodes.
local function py_init()
return
sn(nil, c(1, {
t(""),
sn(1, {
t(", "),
i(1),
d(2, py_init)
})
}))
end
-- splits the string of the comma separated argument list into the arguments
-- and returns the text-/insert- or restore-nodes
local function to_init_assign(args)
local tab = {}
local a = args[1][1]
if #(a) == 0 then
table.insert(tab, t({"", "\tpass"}))
else
local cnt = 1
for e in string.gmatch(a, " ?([^,]*) ?") do
if #e > 0 then
table.insert(tab, t({"","\tself."}))
-- use a restore-node to be able to keep the possibly changed attribute name
-- (otherwise this function would always restore the default, even if the user
-- changed the name)
table.insert(tab, r(cnt, tostring(cnt), i(nil,e)))
table.insert(tab, t(" = "))
table.insert(tab, t(e))
cnt = cnt+1
end
end
end
return
sn(nil, tab)
end
-- create the actual snippet
s("pyinit", fmt(
[[def __init__(self{}):{}]],
{
d(1, py_init),
d(2, to_init_assign, {1})
}))
(Note about the keys: tab is my jump-key, ctrl+j/k is my change_choice-key)
(Being in a choice node is indicated by a blue dot, insert nodes by a white dot at the end of the line)
All - todo-comments.nvim
snippets
As some people using honza/vim-snippets
may know, a convenient use for snippets is to annotate your code with named comments. These comments may include additional information. Typical examples of useful information marks for the comment:
- GitHub username in a repository that has more contributors
- date when browsing code after some time has passed after the writing
- email if you have multiple remotes for the repository and anticipate some other developer needing to contact you
As stated above, some implementations of this are already made for other snippet engines. However, we have LuaSnip
and can supercharge this idea using choice_node
s and lua
.
Instead of inventing the wheel, we will be using the amazing numToStr/Comment.nvim
plugin, which also has added benefits that if the plugin adds compatibility with more languages, we will get them too without any work and that the calculation of the comment-string is based on TreeSitter therefore is also correct for injected languages. Here is a function that fetches comment-strings for us to use in the snippet:
local calculate_comment_string = require('Comment.ft').calculate
local utils = require('Comment.utils')
--- Get the comment string {beg,end} table
---@param ctype integer 1 for `line`-comment and 2 for `block`-comment
---@return table comment_strings {begcstring, endcstring}
local get_cstring = function(ctype)
-- use the `Comments.nvim` API to fetch the comment string for the region (eq. '--%s' or '--[[%s]]' for `lua`)
local cstring = calculate_comment_string { ctype = ctype, range = utils.get_region() } or vim.bo.commentstring
-- as we want only the strings themselves and not strings ready for using `format` we want to split the left and right side
local left, right = utils.unwrap_cstr(cstring)
-- either parts can be nil that's why we check that to return empty string and lastly, create a `{left, right}` table for it
return { left, right }
end
Now we can use this function in any snippet we want. Next we will want some kind of snippet generation based on the input strings, but first, let us define some useful marks that can be placed after the comment.
We will eventually want to place the marks into a choice_node
therefore we want them to return nodes from LuaSnip
. First lets define some variables that we can use in the marks:
_G.luasnip = {}
_G.luasnip.vars = {
username = 'kunzaatko',
email = 'martinkunz@email.cz',
github = 'https://github.com/kunzaatko',
real_name = 'Martin Kunz',
}
Now we can use these in the marks and lets create a variety of them to choose from:
--- Options for marks to be used in a TODO comment
local marks = {
signature = function()
return fmt('<{}>', i(1, _G.luasnip.vars.username))
end,
signature_with_email = function()
return fmt('<{}{}>', { i(1, _G.luasnip.vars.username), i(2, ' ' .. _G.luasnip.vars.email) })
end,
date_signature_with_email = function()
return fmt(
'<{}{}{}>',
{ i(1, os.date '%d-%m-%y'), i(2, ', ' .. _G.luasnip.vars.username), i(3, ' ' .. _G.luasnip.vars.email) }
)
end,
date_signature = function()
return fmt('<{}{}>', { i(1, os.date '%d-%m-%y'), i(2, ', ' .. _G.luasnip.vars.username) })
end,
date = function()
return fmt('<{}>', i(1, os.date '%d-%m-%y'))
end,
empty = function()
return t ''
end,
}
Finally we will want to generate the snippets that we will use for TODO comments. As you can see in the documentation of todo-comments.nvim
, you can create some aliases and we will use them in our snippets too. We want to create snippets that look like this:
<comment-string[1]> [name-of-comment]: {comment-text} [comment-mark] <comment-string[2]>
where <>
are generated strings, []
are choices and {}
are inputs.
Here is an implementation of generating the nodes for the snippet.
local todo_snippet_nodes = function(aliases, opts)
local aliases_nodes = vim.tbl_map(function(alias)
return i(nil, alias) -- generate choices for [name-of-comment]
end, aliases)
local sigmark_nodes = {} -- choices for [comment-mark]
for _, mark in pairs(marks) do
table.insert(sigmark_nodes, mark())
end
-- format them into the actual snippet
local comment_node = fmta('<> <>: <> <> <><>', {
f(function()
return get_cstring(opts.ctype)[1] -- get <comment-string[1]>
end),
c(1, aliases_nodes), -- [name-of-comment]
i(3), -- {comment-text}
c(2, sigmark_nodes), -- [comment-mark]
f(function()
return get_cstring(opts.ctype)[2] -- get <comment-string[2]>
end),
i(0),
})
return comment_node
end
As a cherry on-top, lets make some logic to generate the name
, docstring
and dscr
keys of the context
for the final snippet:
--- Generate a TODO comment snippet with an automatic description and docstring
---@param context table merged with the generated context table `trig` must be specified
---@param aliases string[]|string of aliases for the todo comment (ex.: {FIX, ISSUE, FIXIT, BUG})
---@param opts table merged with the snippet opts table
local todo_snippet = function(context, aliases, opts)
opts = opts or {}
aliases = type(aliases) == 'string' and { aliases } or aliases -- if we do not have aliases, be smart about the function parameters
context = context or {}
if not context.trig then
return error("context doesn't include a `trig` key which is mandatory", 2) -- all we need from the context is the trigger
end
opts.ctype = opts.ctype or 1 -- comment type can be passed in the `opts` table, but if it is not, we have to ensure, it is defined
local alias_string = table.concat(aliases, '|') -- `choice_node` documentation
context.name = context.name or (alias_string .. ' comment') -- generate the `name` of the snippet if not defined
context.dscr = context.dscr or (alias_string .. ' comment with a signature-mark') -- generate the `dscr` if not defined
context.docstring = context.docstring or (' {1:' .. alias_string .. '}: {3} <{2:mark}>{0} ') -- generate the `docstring` if not defined
local comment_node = todo_snippet_nodes(aliases, opts) -- nodes from the previously defined function for their generation
return s(context, comment_node, opts) -- the final todo-snippet constructed from our parameters
end
Now we can create a todo-snippet using only one function with the right parameters. Lets create a few of them:
local todo_snippet_specs = {
{ { trig = 'todo' }, 'TODO' },
{ { trig = 'fix' }, { 'FIX', 'BUG', 'ISSUE', 'FIXIT' } },
{ { trig = 'hack' }, 'HACK' },
{ { trig = 'warn' }, { 'WARN', 'WARNING', 'XXX' } },
{ { trig = 'perf' }, { 'PERF', 'PERFORMANCE', 'OPTIM', 'OPTIMIZE' } },
{ { trig = 'note' }, { 'NOTE', 'INFO' } },
-- NOTE: Block commented todo-comments <kunzaatko>
{ { trig = 'todob' }, 'TODO', { ctype = 2 } },
{ { trig = 'fixb' }, { 'FIX', 'BUG', 'ISSUE', 'FIXIT' }, { ctype = 2 } },
{ { trig = 'hackb' }, 'HACK', { ctype = 2 } },
{ { trig = 'warnb' }, { 'WARN', 'WARNING', 'XXX' }, { ctype = 2 } },
{ { trig = 'perfb' }, { 'PERF', 'PERFORMANCE', 'OPTIM', 'OPTIMIZE' }, { ctype = 2 } },
{ { trig = 'noteb' }, { 'NOTE', 'INFO' }, { ctype = 2 } },
}
local todo_comment_snippets = {}
for _, v in ipairs(todo_snippet_specs) do
-- NOTE: 3rd argument accepts nil
table.insert(todo_comment_snippets, todo_snippet(v[1], v[2], v[3]))
end
ls.add_snippets('all', todo_comment_snippets, { type = 'snippets', key = 'todo_comments' })
And a video of the final result: