Skip to content
numToStr edited this page Aug 22, 2022 · 52 revisions

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 ;)

Latex - Endless list

⚠️ This is pretty hackish, and is just meant to how far luasnip can be pushed. For actual use, the external update DynamicNode is more capable, snippets are more readable, and its behaviour in is plain better (eg. doesn't lead to deeply nested snippets)
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 \items' as desired.

ls_showcase

(The gif also showcases Luasnips ability to preserve input to collapsed choices!)

Latex - Context-aware snippets

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.

Latex - Tabular

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}"}
}),

Peek 2022-06-01 16-31

All - Pairs

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.

All - Insert space when the next character after snippet is a letter

  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.

Python - function definition with dynamic virtual text

The following snippet expands to a function definition in Python and has choiceNodes 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),
	}))
})

luasnip_choicenode

Python - init function with dynamic initializer list

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})
  }))

Peek 2022-03-21 15-00 (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:

  1. GitHub username in a repository that has more contributors
  2. date when browsing code after some time has passed after the writing
  3. 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_nodes and lua.

Finding the comment-string

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.

Info marks 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,
}

todo-comments snippets

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: todo_comments_video

Clone this wiki locally