1

Update generated nvim config

This commit is contained in:
2024-06-03 20:37:40 +02:00
parent 8c55c21341
commit 83e285850f
295 changed files with 32868 additions and 0 deletions

View File

@ -0,0 +1,8 @@
---@alias LuaSnip.Cursor {[1]: number, [2]: number}
---@class LuaSnip.MatchRegion 0-based region
---@field row integer 0-based row
---@field col_range { [1]: integer, [2]: integer } 0-based column range, from-in, to-exclusive
---@alias LuaSnip.Addable table
---Anything that can be passed to ls.add_snippets().

View File

@ -0,0 +1,143 @@
local ext_util = require("luasnip.util.ext_opts")
local session = require("luasnip.session")
local conf_defaults = require("luasnip.default_config")
local function set_snip_env(target_conf_defaults, user_config)
if not user_config.snip_env then
-- target_conf already contains defaults
return
end
-- either "set" or "extend", make sure it does not appear in the final snip_env.
local snip_env_behaviour = user_config.snip_env.__snip_env_behaviour ~= nil
and user_config.snip_env.__snip_env_behaviour
or "extend"
assert(
snip_env_behaviour == "set" or snip_env_behaviour == "extend",
"Unknown __snip_env_behaviour, `" .. snip_env_behaviour .. "`"
)
user_config.snip_env.__snip_env_behaviour = nil
if snip_env_behaviour == "set" then
target_conf_defaults.snip_env = user_config.snip_env
else
-- cannot use vim.tbl_extend, since we'd need to transfer the metatable.
for k, v in pairs(user_config.snip_env) do
target_conf_defaults.snip_env[k] = v
end
end
-- set to nil, to mark that it's handled.
user_config.snip_env = nil
end
-- declare here to use in set_config.
local c
c = {
set_config = function(user_config)
user_config = user_config or {}
local conf = vim.deepcopy(conf_defaults)
-- remove unused highlights from default-ext_opts.
ext_util.clear_invalid(conf.ext_opts)
conf.ext_opts = ext_util.child_complete(conf.ext_opts)
user_config.ext_opts =
ext_util.child_complete(user_config.ext_opts or {})
ext_util.child_extend(user_config.ext_opts, conf.ext_opts)
-- use value from update_events, then updateevents.
-- also nil updateevents, don't spill it into the main config.
user_config.update_events = user_config.update_events
or user_config.updateevents
user_config.updateevents = nil
set_snip_env(conf, user_config)
-- handle legacy-key history.
if user_config.history ~= nil then
conf.keep_roots = user_config.history
conf.link_roots = user_config.history
conf.exit_roots = not user_config.history
conf.link_children = user_config.history
-- unset key to prevent handling twice.
conf.history = nil
end
for k, v in pairs(user_config) do
conf[k] = v
end
session.config = conf
c._setup()
end,
_setup = function()
local augroup = vim.api.nvim_create_augroup("luasnip", {})
-- events: string[], or string. if string[], each string is one
-- event-name, if string, either one event-name, or multiple delimited by `,`.
local function ls_autocmd(events, callback)
if type(events) == "string" then
-- split on ',' for backwards compatibility.
-- remove spaces from string.
events = vim.split(events:gsub(" ", ""), ",")
end
vim.api.nvim_create_autocmd(events, {
callback = callback,
group = augroup,
})
end
if session.config.delete_check_events ~= nil then
ls_autocmd(
session.config.delete_check_events,
require("luasnip").unlink_current_if_deleted
)
end
ls_autocmd(
session.config.update_events,
require("luasnip").active_update_dependents
)
if session.config.region_check_events ~= nil then
ls_autocmd(session.config.region_check_events, function()
require("luasnip").exit_out_of_region(
require("luasnip").session.current_nodes[vim.api.nvim_get_current_buf()]
)
end)
end
-- Remove buffers' nodes on deletion+wipeout.
ls_autocmd({ "BufDelete", "BufWipeout" }, function(event)
local current_nodes = require("luasnip").session.current_nodes
if current_nodes then
current_nodes[event.buf] = nil
end
end)
if session.config.enable_autosnippets then
ls_autocmd("InsertCharPre", function()
Luasnip_just_inserted = true
end)
ls_autocmd({ "TextChangedI", "TextChangedP" }, function()
if Luasnip_just_inserted then
require("luasnip").expand_auto()
Luasnip_just_inserted = nil
end
end)
end
if session.config.store_selection_keys then
vim.cmd(
string.format(
[[xnoremap <silent> %s %s]],
session.config.store_selection_keys,
require("luasnip.util.select").select_keys
)
)
end
end,
}
-- Keep these two for backward compativility
c.setup = c.set_config
return c

View File

@ -0,0 +1,213 @@
local types = require("luasnip.util.types")
local lazy_table = require("luasnip.util.lazy_table")
local ft_functions = require("luasnip.extras.filetype_functions")
-- Inserts a insert(1) before all other nodes, decreases node.pos's as indexing is "wrong".
local function modify_nodes(snip)
for i = #snip.nodes, 1, -1 do
snip.nodes[i + 1] = snip.nodes[i]
local node = snip.nodes[i + 1]
if node.pos then
node.pos = node.pos + 1
end
end
local iNode = require("luasnip.nodes.insertNode")
snip.nodes[1] = iNode.I(1)
end
local lazy_snip_env = {
s = function()
return require("luasnip.nodes.snippet").S
end,
sn = function()
return require("luasnip.nodes.snippet").SN
end,
isn = function()
return require("luasnip.nodes.snippet").ISN
end,
t = function()
return require("luasnip.nodes.textNode").T
end,
i = function()
return require("luasnip.nodes.insertNode").I
end,
f = function()
return require("luasnip.nodes.functionNode").F
end,
c = function()
return require("luasnip.nodes.choiceNode").C
end,
d = function()
return require("luasnip.nodes.dynamicNode").D
end,
r = function()
return require("luasnip.nodes.restoreNode").R
end,
events = function()
return require("luasnip.util.events")
end,
k = function()
return require("luasnip.nodes.key_indexer").new_key
end,
ai = function()
return require("luasnip.nodes.absolute_indexer")
end,
extras = function()
return require("luasnip.extras")
end,
l = function()
return require("luasnip.extras").lambda
end,
rep = function()
return require("luasnip.extras").rep
end,
p = function()
return require("luasnip.extras").partial
end,
m = function()
return require("luasnip.extras").match
end,
n = function()
return require("luasnip.extras").nonempty
end,
dl = function()
return require("luasnip.extras").dynamic_lambda
end,
fmt = function()
return require("luasnip.extras.fmt").fmt
end,
fmta = function()
return require("luasnip.extras.fmt").fmta
end,
conds = function()
return require("luasnip.extras.expand_conditions")
end,
postfix = function()
return require("luasnip.extras.postfix").postfix
end,
types = function()
return require("luasnip.util.types")
end,
parse = function()
return require("luasnip.util.parser").parse_snippet
end,
ms = function()
return require("luasnip.nodes.multiSnippet").new_multisnippet
end,
}
-- stylua: ignore
return {
-- corresponds to legacy "history=false".
keep_roots = false,
link_roots = false,
exit_roots = true,
link_children = false,
update_events = "InsertLeave",
-- see :h User, event should never be triggered(except if it is `doautocmd`'d)
region_check_events = nil,
delete_check_events = nil,
store_selection_keys = nil, -- Supossed to be the same as the expand shortcut
ext_opts = {
[types.textNode] = {
active = { hl_group = "LuasnipTextNodeActive" },
passive = { hl_group = "LuasnipTextNodePassive" },
visited = { hl_group = "LuasnipTextNodeVisited" },
unvisited = { hl_group = "LuasnipTextNodeUnvisited" },
snippet_passive = { hl_group = "LuasnipTextNodeSnippetPassive" },
},
[types.insertNode] = {
active = { hl_group = "LuasnipInsertNodeActive" },
passive = { hl_group = "LuasnipInsertNodePassive" },
visited = { hl_group = "LuasnipInsertNodeVisited" },
unvisited = { hl_group = "LuasnipInsertNodeUnvisited" },
snippet_passive = {
hl_group = "LuasnipInsertNodeSnippetPassive",
},
},
[types.exitNode] = {
active = { hl_group = "LuasnipExitNodeActive" },
passive = { hl_group = "LuasnipExitNodePassive" },
visited = { hl_group = "LuasnipExitNodeVisited" },
unvisited = { hl_group = "LuasnipExitNodeUnvisited" },
snippet_passive = { hl_group = "LuasnipExitNodeSnippetPassive" },
},
[types.functionNode] = {
active = { hl_group = "LuasnipFunctionNodeActive" },
passive = { hl_group = "LuasnipFunctionNodePassive" },
visited = { hl_group = "LuasnipFunctionNodeVisited" },
unvisited = { hl_group = "LuasnipFunctionNodeUnvisited" },
snippet_passive = {
hl_group = "LuasnipFunctionNodeSnippetPassive",
},
},
[types.snippetNode] = {
active = { hl_group = "LuasnipSnippetNodeActive" },
passive = { hl_group = "LuasnipSnippetNodePassive" },
visited = { hl_group = "LuasnipSnippetNodeVisited" },
unvisited = { hl_group = "LuasnipSnippetNodeUnvisited" },
snippet_passive = {
hl_group = "LuasnipSnippetNodeSnippetPassive",
},
},
[types.choiceNode] = {
active = { hl_group = "LuasnipChoiceNodeActive" },
passive = { hl_group = "LuasnipChoiceNodePassive" },
visited = { hl_group = "LuasnipChoiceNodeVisited" },
unvisited = { hl_group = "LuasnipChoiceNodeUnvisited" },
snippet_passive = {
hl_group = "LuasnipChoiceNodeSnippetPassive",
},
},
[types.dynamicNode] = {
active = { hl_group = "LuasnipDynamicNodeActive" },
passive = { hl_group = "LuasnipDynamicNodePassive" },
visited = { hl_group = "LuasnipDynamicNodeVisited" },
unvisited = { hl_group = "LuasnipDynamicNodeUnvisited" },
snippet_passive = {
hl_group = "LuasnipDynamicNodeSnippetPassive",
},
},
[types.snippet] = {
active = { hl_group = "LuasnipSnippetActive" },
passive = { hl_group = "LuasnipSnippetPassive" },
-- not used!
visited = { hl_group = "LuasnipSnippetVisited" },
unvisited = { hl_group = "LuasnipSnippetUnvisited" },
snippet_passive = { hl_group = "LuasnipSnippetSnippetPassive" },
},
[types.restoreNode] = {
active = { hl_group = "LuasnipRestoreNodeActive" },
passive = { hl_group = "LuasnipRestoreNodePassive" },
visited = { hl_group = "LuasnipRestoreNodeVisited" },
unvisited = { hl_group = "LuasnipRestoreNodeUnvisited" },
snippet_passive = {
hl_group = "LuasnipRestoreNodeSnippetPassive",
},
},
},
ext_base_prio = 200,
ext_prio_increase = 9,
enable_autosnippets = false,
parser_nested_assembler = function(pos, snip)
-- only require here, to prevent some upfront load-cost.
local iNode = require("luasnip.nodes.insertNode")
local cNode = require("luasnip.nodes.choiceNode")
modify_nodes(snip)
snip:init_nodes()
snip.pos = nil
return cNode.C(pos, { snip, iNode.I(nil, { "" }) })
end,
-- Function expected to return a list of filetypes (or empty list)
ft_func = ft_functions.from_filetype,
-- fn(bufnr) -> string[] (filetypes).
load_ft_func = ft_functions.from_filetype_load,
-- globals injected into luasnippet-files.
snip_env = lazy_table({}, lazy_snip_env),
loaders_store_source = false,
}

View File

@ -0,0 +1,26 @@
---@class LuaSnip.extra.MatchTSNodeOpts Passed in by the user, describes how to
---select a node from the tree via a query and captures.
---@field query? string A query, as text
---@field query_name? string The name of a query (passed to `vim.treesitter.query.get`).
---@field query_lang string The language of the query.
---@field select? LuaSnip.extra.BuiltinMatchSelector|LuaSnip.extra.MatchSelector
---@field match_captures? string|string[]
---@class LuaSnip.extra.MatchedTSNodeInfo
---@field capture_name string
---@field node TSNode
---@alias LuaSnip.extra.BuiltinMatchSelector
---| '"any"' # The default selector, selects the first match but not return all captures
---| '"shortest"' # Selects the shortest match, return all captures too
---| '"longest"' # Selects the longest match, return all captures too
---Call record repeatedly to record all matches/nodes, retrieve once there are no more matches
---@class LuaSnip.extra.MatchSelector
---@field record fun(ts_match: TSMatch?, node: TSNode): boolean return true if recording can be aborted
--- (because the best match has been found)
---@field retrieve fun(): TSMatch?,TSNode? return the best match, as determined by this selector.
---@alias LuaSnip.extra.MatchTSNodeFunc fun(parser: LuaSnip.extra.TSParser, cursor: LuaSnip.Cursor): LuaSnip.extra.NamedTSMatch?,TSNode?
---@alias LuaSnip.extra.NamedTSMatch table<string,TSNode>

View File

@ -0,0 +1,377 @@
-- Mostly borrowed from https://github.com/lunarmodules/Penlight/ with just some changes to use
-- neovim internal functions and reformat
-- Copyright (C) 2009-2016 Steve Donovan, David Manura.
local concat, append = table.concat, table.insert
local map = vim.tbl_map
local _DEBUG = rawget(_G, "_DEBUG")
local function assert_arg(n, val, tp, verify, msg, lev)
if type(val) ~= tp then
error(
("argument %d expected a '%s', got a '%s'"):format(n, tp, type(val)),
lev or 2
)
end
if verify and not verify(val) then
error(("argument %d: '%s' %s"):format(n, val, msg), lev or 2)
end
end
local lambda = {}
-- metatable for Placeholder Expressions (PE)
local _PEMT = {}
local function P(t)
setmetatable(t, _PEMT)
return t
end
lambda.PE = P
local function isPE(obj)
return getmetatable(obj) == _PEMT
end
lambda.isPE = isPE
-- construct a placeholder variable (e.g _1 and _2)
local function PH(idx)
return P({ op = "X", repr = "args[" .. idx .. "]", index = idx })
end
-- construct a constant placeholder variable (e.g _C1 and _C2)
local function CPH(idx)
return P({ op = "X", repr = "_C" .. idx, index = idx })
end
lambda._1, lambda._2, lambda._3, lambda._4, lambda._5 =
PH(1), PH(2), PH(3), PH(4), PH(5)
lambda._0 = P({ op = "X", repr = "...", index = 0 })
function lambda.Var(name)
local ls = vim.split(name, "[%s,]+")
local res = {}
for i = 1, #ls do
append(res, P({ op = "X", repr = ls[i], index = 0 }))
end
return unpack(res)
end
function lambda._(value)
return P({ op = "X", repr = value, index = "wrap" })
end
-- unknown keys are some named variable.
setmetatable(lambda, {
__index = function(_, key)
-- \\n to be correctly interpreted in `load()`.
return P({
op = "X",
repr = "args." .. key,
index = 0,
})
end,
})
local repr
lambda.Nil = lambda.Var("nil")
function _PEMT.__index(obj, key)
return P({ op = "[]", obj, key })
end
function _PEMT.__call(fun, ...)
return P({ op = "()", fun, ... })
end
function _PEMT.__tostring(e)
return repr(e)
end
function _PEMT.__unm(arg)
return P({ op = "unm", arg })
end
function lambda.Not(arg)
return P({ op = "not", arg })
end
function lambda.Len(arg)
return P({ op = "#", arg })
end
local function binreg(context, t)
for name, op in pairs(t) do
rawset(context, name, function(x, y)
return P({ op = op, x, y })
end)
end
end
local function import_name(name, fun, context)
rawset(context, name, function(...)
return P({ op = "()", fun, ... })
end)
end
local imported_functions = {}
local function is_global_table(n)
return type(_G[n]) == "table"
end
--- wrap a table of functions. This makes them available for use in
-- placeholder expressions.
-- @string tname a table name
-- @tab context context to put results, defaults to environment of caller
function lambda.import(tname, context)
assert_arg(
1,
tname,
"string",
is_global_table,
"arg# 1: not a name of a global table"
)
local t = _G[tname]
context = context or _G
for name, fun in pairs(t) do
import_name(name, fun, context)
imported_functions[fun] = name
end
end
--- register a function for use in placeholder expressions.
-- @lambda fun a function
-- @string[opt] name an optional name
-- @return a placeholder functiond
function lambda.register(fun, name)
assert_arg(1, fun, "function")
if name then
assert_arg(2, name, "string")
imported_functions[fun] = name
end
return function(...)
return P({ op = "()", fun, ... })
end
end
function lambda.lookup_imported_name(fun)
return imported_functions[fun]
end
local function _arg(...)
return ...
end
function lambda.Args(...)
return P({ op = "()", _arg, ... })
end
-- binary operators with their precedences (see Lua manual)
-- precedences might be incremented by one before use depending on
-- left- or right-associativity, space them out
local binary_operators = {
["or"] = 0,
["and"] = 2,
["=="] = 4,
["~="] = 4,
["<"] = 4,
[">"] = 4,
["<="] = 4,
[">="] = 4,
[".."] = 6,
["+"] = 8,
["-"] = 8,
["*"] = 10,
["/"] = 10,
["%"] = 10,
["^"] = 14,
}
-- unary operators with their precedences
local unary_operators = {
["not"] = 12,
["#"] = 12,
["unm"] = 12,
}
-- comparisons (as prefix functions)
binreg(lambda, {
And = "and",
Or = "or",
Eq = "==",
Lt = "<",
Gt = ">",
Le = "<=",
Ge = ">=",
})
-- standard binary operators (as metamethods)
binreg(_PEMT, {
__add = "+",
__sub = "-",
__mul = "*",
__div = "/",
__mod = "%",
__pow = "^",
__concat = "..",
})
binreg(_PEMT, { __eq = "==" })
--- all elements of a table except the first.
-- @tab ls a list-like table.
function lambda.tail(ls)
assert_arg(1, ls, "table")
local res = {}
for i = 2, #ls do
append(res, ls[i])
end
return res
end
--- create a string representation of a placeholder expression.
-- @param e a placeholder expression
-- @param lastpred not used
function repr(e, lastpred)
local tail = lambda.tail
if isPE(e) then
local pred = binary_operators[e.op] or unary_operators[e.op]
if pred then
-- binary or unary operator
local s
if binary_operators[e.op] then
local left_pred = pred
local right_pred = pred
if e.op == ".." or e.op == "^" then
left_pred = left_pred + 1
else
right_pred = right_pred + 1
end
local left_arg = repr(e[1], left_pred)
local right_arg = repr(e[2], right_pred)
s = left_arg .. " " .. e.op .. " " .. right_arg
else
local op = e.op == "unm" and "-" or e.op
s = op .. " " .. repr(e[1], pred)
end
if lastpred and lastpred > pred then
s = "(" .. s .. ")"
end
return s
else -- either postfix, or a placeholder
local ls = map(repr, e)
if e.op == "[]" then
return ls[1] .. "[" .. ls[2] .. "]"
elseif e.op == "()" then
local fn
if ls[1] ~= nil then -- was _args, undeclared!
fn = ls[1]
else
fn = ""
end
return fn .. "(" .. concat(tail(ls), ",") .. ")"
else
return e.repr
end
end
elseif type(e) == "string" then
return '"' .. e .. '"'
elseif type(e) == "function" then
local name = lambda.lookup_imported_name(e)
if name then
return name
else
return tostring(e)
end
else
return tostring(e) --should not really get here!
end
end
lambda.repr = repr
-- collect all the non-PE values in this PE into vlist, and replace each occurence
-- with a constant PH (_C1, etc). Return the maximum placeholder index found.
local collect_values
function collect_values(e, vlist)
if isPE(e) then
if e.op ~= "X" then
local m = 0
for i = 1, #e do
local subx = e[i]
local pe = isPE(subx)
if pe then
if subx.op == "X" and subx.index == "wrap" then
subx = subx.repr
pe = false
else
m = math.max(m, collect_values(subx, vlist))
end
end
if not pe then
append(vlist, subx)
e[i] = CPH(#vlist)
end
end
return m
else -- was a placeholder, it has an index...
return e.index
end
else -- plain value has no placeholder dependence
return 0
end
end
lambda.collect_values = collect_values
--- instantiate a PE into an actual function. First we find the largest placeholder used,
-- e.g. _2; from this a list of the formal parameters can be build. Then we collect and replace
-- any non-PE values from the PE, and build up a constant binding list.
-- Finally, the expression can be compiled, and e.__PE_function is set.
-- @param e a placeholder expression
-- @return a function
function lambda.instantiate(e)
local consts, values = {}, {}
local rep, err, fun
local n = lambda.collect_values(e, values)
for i = 1, #values do
append(consts, "_C" .. i)
if _DEBUG then
print(i, values[i])
end
end
consts = concat(consts, ",")
rep = repr(e)
local fstr = ("return function(%s) return function(args) return %s end end"):format(
consts,
rep
)
if _DEBUG then
print(fstr)
end
fun, err = load(fstr, "fun")
if not fun then
return nil, err
end
fun = fun() -- get wrapper
fun = fun(unpack(values)) -- call wrapper (values could be empty)
e.__PE_function = fun
return fun
end
--- instantiate a PE unless it has already been done.
-- @param e a placeholder expression
-- @return the function
function lambda.I(e)
if rawget(e, "__PE_function") then
return e.__PE_function
else
return lambda.instantiate(e)
end
end
return lambda

View File

@ -0,0 +1,94 @@
-- Minimal parser combinator,
-- only for internal use so not exposed elsewhere nor documented in the oficial doc
--
local M = {}
-- Consumes strings matching a pattern, generates the matched string
function M.pattern(pat)
return function(text, pos)
local s, e = text:find(pat, pos)
if s then
local v = text:sub(s, e)
return true, v, pos + #v
else
return false, nil, pos
end
end
end
-- Matches whatever `p matches and generates whatever p generates after
-- transforming it with `f
function M.map(p, f)
return function(text, pos)
local succ, val, new_pos = p(text, pos)
if succ then
return true, f(val), new_pos
end
return false, nil, pos
end
end
-- Matches and generates the same as the first of it's children that matches something
function M.any(...)
local parsers = { ... }
return function(text, pos)
for _, p in ipairs(parsers) do
local succ, val, new_pos = p(text, pos)
if succ then
return true, val, new_pos
end
end
return false, nil, pos
end
end
-- Matches all what its children do in sequence, generates a table of its children generations
function M.seq(...)
local parsers = { ... }
return function(text, pos)
local original_pos = pos
local values = {}
for _, p in ipairs(parsers) do
local succ, val, new_pos = p(text, pos)
pos = new_pos
if not succ then
return false, nil, original_pos
end
table.insert(values, val)
end
return true, values, pos
end
end
-- Matches cero or more times what it child do in sequence, generates a table with those generations
function M.star(p)
return function(text, pos)
local len = #text
local values = {}
while pos <= len do
local succ, val, new_pos = p(text, pos)
if succ then
pos = new_pos
table.insert(values, val)
else
break
end
end
return #values > 0, values, pos
end
end
-- Consumes a literal string, does not generates
function M.literal(t)
return function(text, pos)
if text:sub(pos, pos + #t - 1) == t then
return true, nil, pos + #t
else
return false, text:sub(pos, pos + #t), pos + #t
end
end
end
return M

View File

@ -0,0 +1,433 @@
local util = require("luasnip.util.util")
local tbl = require("luasnip.util.table")
local function get_lang(bufnr)
local ft = vim.api.nvim_buf_get_option(bufnr, "ft")
local lang = vim.treesitter.language.get_lang(ft) or ft
return lang
end
-- Inspect node
---@param node TSNode?
---@return string
local function inspect_node(node)
if node == nil then
return "nil"
end
local start_row, start_col, end_row, end_col =
vim.treesitter.get_node_range(node)
return ("%s [%d, %d] [%d, %d]"):format(
node:type(),
start_row,
start_col,
end_row,
end_col
)
end
---@param bufnr number
---@param region LuaSnip.MatchRegion
---@return LanguageTree, string
local function reparse_buffer_after_removing_match(bufnr, region)
local lang = get_lang(bufnr)
-- have to get entire buffer, a pattern-match may include lines behind the trigger.
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
-- region is 0-indexed, lines and strings 1-indexed.
local region_line = lines[region.row + 1]
-- sub includes end, want to exclude it.
local left_part = region_line:sub(1, region.col_range[1] + 1 - 1)
local right_part = region_line:sub(region.col_range[2] + 1)
lines[region.row + 1] = left_part .. right_part
local source = table.concat(lines, "\n")
---@type LanguageTree
local parser = vim.treesitter.get_string_parser(source, lang, nil)
parser:parse()
return parser, source
end
---@class LuaSnip.extra.FixBufferContext
---@field ori_bufnr number
---@field ori_text string
---@field region LuaSnip.MatchRegion
local FixBufferContext = {}
---@param ori_bufnr number
---@param region LuaSnip.MatchRegion
---@return LuaSnip.extra.FixBufferContext
function FixBufferContext.new(ori_bufnr, region, region_content)
local o = {
ori_bufnr = ori_bufnr,
ori_text = region_content,
region = region,
}
setmetatable(o, {
__index = FixBufferContext,
})
return o
end
function FixBufferContext:enter()
vim.api.nvim_buf_set_text(
self.ori_bufnr,
self.region.row,
self.region.col_range[1],
self.region.row,
self.region.col_range[2],
{ "" }
)
local parser, source =
vim.treesitter.get_parser(self.ori_bufnr), self.ori_bufnr
parser:parse()
return parser, source
end
function FixBufferContext:leave()
vim.api.nvim_buf_set_text(
self.ori_bufnr,
self.region.row,
self.region.col_range[1],
self.region.row,
self.region.col_range[1],
{ self.ori_text }
)
-- The cursor does not necessarily move with the insertion, and has to be
-- restored manually.
-- when making this work for expansion away from cursor, store cursor-pos
-- in self.
vim.api.nvim_win_set_cursor(
0,
{ self.region.row + 1, self.region.col_range[2] }
)
local parser, source =
vim.treesitter.get_parser(self.ori_bufnr), self.ori_bufnr
parser:parse()
return parser, source
end
-- iterate over all
local function captures_iter(captures)
-- turn string/string[] into map: string -> bool, for querying whether some
-- string is present in captures.
local capture_map = tbl.list_to_set(captures)
-- receives the query and the iterator over all its matches.
return function(query, match_iter)
local current_match
local current_capture_id
local iter
local pattern
iter = function()
-- if there is no current match to continue,
if not current_match then
pattern, current_match, _ = match_iter()
-- occurs once there are no more matches.
if not pattern then
return nil
end
end
while true do
local node
current_capture_id, node =
next(current_match, current_capture_id)
if not current_capture_id then
break
end
local capture_name = query.captures[current_capture_id]
if capture_map[capture_name] then
return current_match, node
end
end
-- iterated over all captures of the current match, reset it to
-- retrieve the next match in the recursion.
current_match = nil
-- tail-call-optimization! :fingers_crossed:
return iter()
end
return iter
end
end
local builtin_tsnode_selectors = {
any = function()
local best_node
local best_node_match
return {
record = function(match, node)
best_node = node
best_node_match = match
-- abort immediately, we just want any match.
return true
end,
retrieve = function()
return best_node_match, best_node
end,
}
end,
shortest = function()
local best_node
local best_node_match
-- end is already equal, only have to compare start.
local best_node_start
return {
record = function(match, node)
local start_row, start_col, _, _ =
vim.treesitter.get_node_range(node)
if
(best_node == nil)
or (start_row > best_node_start[1])
or (
start_row == best_node_start[1]
and start_col > best_node_start[2]
)
then
best_node = node
best_node_match = match
best_node_start = { start_row, start_col }
end
-- don't abort, have to see all potential nodes to find shortest match.
return false
end,
retrieve = function()
return best_node_match, best_node
end,
}
end,
longest = function()
local best_node
local best_node_match
-- end is already equal, only have to compare start.
local best_node_start
return {
record = function(match, node)
local start_row, start_col, _, _ =
vim.treesitter.get_node_range(node)
if
(best_node == nil)
or (start_row < best_node_start[1])
or (
start_row == best_node_start[1]
and start_col < best_node_start[2]
)
then
best_node = node
best_node_match = match
best_node_start = { start_row, start_col }
end
-- don't abort, have to see all potential nodes to find longest match.
return false
end,
retrieve = function()
return best_node_match, best_node
end,
}
end,
}
---@class LuaSnip.extra.TSParser
---@field parser LanguageTree
---@field source string|number
local TSParser = {}
---@param bufnr number?
---@param parser LanguageTree
---@param source string|number
---@return LuaSnip.extra.TSParser?
function TSParser.new(bufnr, parser, source)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local o = {
parser = parser,
source = source,
}
setmetatable(o, {
__index = TSParser,
---@param self LuaSnip.extra.TSParser
---@return string
__tostring = function(self)
return ("trees: %d, source: %s"):format(
#self.parser:trees(),
type(self.source) == "number" and tostring(self.source)
or "[COPIED]"
)
end,
})
return o
end
---@param pos { [1]: number, [2]: number }?
---@return TSNode?
function TSParser:get_node_at_pos(pos)
pos = vim.F.if_nil(pos, util.get_cursor_0ind())
local row, col = pos[1], pos[2]
assert(
row >= 0 and col >= 0,
"Invalid position: row and col must be non-negative"
)
local range = { row, col, row, col }
return self.parser:named_node_for_range(
range,
{ ignore_injections = false }
)
end
---Get the root for the smallest tree containing `pos`.
---@param pos { [1]: number, [2]: number }
---@return TSNode?
function TSParser:root_at(pos)
local tree = self.parser:tree_for_range(
{ pos[1], pos[2], pos[1], pos[2] },
{ ignore_injections = false }
)
if not tree then
return nil
end
return tree:root()
end
---@param match_opts LuaSnip.extra.EffectiveMatchTSNodeOpts
---@param pos { [1]: number, [2]: number }
---@return LuaSnip.extra.NamedTSMatch?, TSNode?
function TSParser:match_at(match_opts, pos)
-- Since we want to find a match to the left of pos, and if we accept there
-- has to be at least one character (I assume), we should probably not look
-- for the tree containing `pos`, since that might be the wrong one (if
-- injected languages are in play).
local root = self:root_at({ pos[1], pos[2] - 1 })
if root == nil then
return nil, nil
end
local root_from_line, _, root_to_line, _ = root:range()
local query = match_opts.query
local selector = match_opts.selector()
local next_ts_match =
-- end-line is excluded by iter_matches, if the column of root_to
-- greater than 0, we would erroneously ignore a line that could
-- contain our match.
query:iter_matches(root, self.source, root_from_line, root_to_line + 1)
for match, node in match_opts.generator(query, next_ts_match) do
-- false: don't include bytes.
local _, _, end_row, end_col = node:range(false)
if end_row == pos[1] and end_col == pos[2] then
if selector.record(match, node) then
-- should abort iteration
break
end
end
end
local best_match, node = selector.retrieve()
if not best_match then
return nil, nil
end
-- map captures via capture-name, not id.
local named_captures_match = {}
for id, capture_node in pairs(best_match) do
named_captures_match[query.captures[id]] = capture_node
end
return named_captures_match, node
end
---@param node TSNode
---@return string
function TSParser:get_node_text(node)
-- not sure what happens if this is multiline.
return vim.treesitter.get_node_text(node, self.source)
end
---@param root TSNode
---@param n number
---@param matcher fun(node:TSNode):boolean|nil
---@return TSNode?
local function find_nth_parent(root, n, matcher)
local parent = root
matcher = matcher or function()
return true
end
local i = 0
while i < n do
if not parent or not matcher(parent) then
return nil
end
parent = parent:parent()
i = i + 1
end
if not parent or not matcher(parent) then
return nil
end
return parent
end
---@param root TSNode
---@param matcher fun(node:TSNode):boolean|nil
local function find_topmost_parent(root, matcher)
---@param node TSNode?
---@return TSNode?
local function _impl(node)
if node == nil then
return nil
end
local current = nil
if matcher == nil or matcher(node) then
current = node
end
return vim.F.if_nil(_impl(node:parent()), current)
end
return _impl(root)
end
---@param root TSNode
---@param matcher fun(node:TSNode):boolean|nil
local function find_first_parent(root, matcher)
---@param node TSNode?
---@return TSNode?
local function _impl(node)
if node == nil then
return nil
end
if matcher == nil or matcher(node) then
return node
end
return _impl(node:parent())
end
return _impl(root)
end
return {
get_lang = get_lang,
reparse_buffer_after_removing_match = reparse_buffer_after_removing_match,
TSParser = TSParser,
FixBufferContext = FixBufferContext,
find_topmost_parent = find_topmost_parent,
find_first_parent = find_first_parent,
find_nth_parent = find_nth_parent,
inspect_node = inspect_node,
captures_iter = captures_iter,
builtin_tsnode_selectors = builtin_tsnode_selectors,
}

View File

@ -0,0 +1,14 @@
local cond_obj = require("luasnip.extras.conditions")
-- use the functions from show as basis and extend/overwrite functions specific for expand here
local M = vim.deepcopy(require("luasnip.extras.conditions.show"))
-----------------------
-- PRESET CONDITIONS --
-----------------------
local function line_begin(line_to_cursor, matched_trigger)
-- +1 because `string.sub("abcd", 1, -2)` -> abc
return line_to_cursor:sub(1, -(#matched_trigger + 1)):match("^%s*$")
end
M.line_begin = cond_obj.make_condition(line_begin)
return M

View File

@ -0,0 +1,55 @@
local M = {}
-----------------------
-- CONDITION OBJECTS --
-----------------------
local condition_mt = {
-- logic operators
-- not '-'
__unm = function(o1)
return M.make_condition(function(...)
return not o1(...)
end)
end,
-- or '+'
__add = function(o1, o2)
return M.make_condition(function(...)
return o1(...) or o2(...)
end)
end,
__sub = function(o1, o2)
return M.make_condition(function(...)
return o1(...) and not o2(...)
end)
end,
-- and '*'
__mul = function(o1, o2)
return M.make_condition(function(...)
return o1(...) and o2(...)
end)
end,
-- xor '^'
__pow = function(o1, o2)
return M.make_condition(function(...)
return o1(...) ~= o2(...)
end)
end,
-- xnor '%'
-- might be counter intuitive, but as we can't use '==' (must return bool)
-- it's best to use something weird (doesn't have to be used)
__mod = function(o1, o2)
return function(...)
return o1(...) == o2(...)
end
end,
-- use table like a function by overloading __call
__call = function(tab, line_to_cursor, matched_trigger, captures)
return tab.func(line_to_cursor, matched_trigger, captures)
end,
}
function M.make_condition(func)
return setmetatable({ func = func }, condition_mt)
end
return M

View File

@ -0,0 +1,21 @@
local cond_obj = require("luasnip.extras.conditions")
local M = {}
-----------------------
-- PRESET CONDITIONS --
-----------------------
local function line_end(line_to_cursor)
local line = vim.api.nvim_get_current_line()
-- looks pretty inefficient, but as lue interns strings, this is just a
-- comparision of pointers (which probably is faster than calculate the
-- length and then checking)
return line_to_cursor == line
end
M.line_end = cond_obj.make_condition(line_end)
local function has_selected_text()
return vim.b.LUASNIP_TM_SELECT ~= nil
end
M.has_selected_text = cond_obj.make_condition(has_selected_text)
return M

View File

@ -0,0 +1 @@
return require("luasnip.extras.conditions.expand")

View File

@ -0,0 +1,89 @@
local function fts_from_ts_lang(lang)
local fts = {}
-- In case of someone using nvim <= 0.9
if vim.treesitter.language and vim.treesitter.language.get_filetypes then
fts = vim.treesitter.language.get_filetypes(lang)
end
-- Keep lang as part of the result, for backward compatibility.
-- If lang is already part of fts, one entry will be removed by deduplicate
-- in get_snippet_filetypes().
table.insert(fts, lang)
return fts
end
local function from_cursor_pos()
-- get_parser errors if parser not present (no grammar for language).
local has_parser, parser = pcall(vim.treesitter.get_parser)
if has_parser then
local cursor = require("luasnip.util.util").get_cursor_0ind()
-- assumption: languagetree uses 0-indexed byte-ranges.
local lang = parser
:language_for_range({
cursor[1],
cursor[2],
cursor[1],
cursor[2],
})
:lang()
return fts_from_ts_lang(lang)
else
return {}
end
end
local function from_filetype()
return vim.split(vim.bo.filetype, ".", { plain = true, trimemtpy = false })
end
-- NOTE: Beware that the resulting filetypes may differ from the ones in `vim.bo.filetype`. (for
-- example the filetype for LaTeX is 'latex' and not 'tex' as in `vim.bo.filetype`) --
local function from_pos_or_filetype()
local from_cursor = from_cursor_pos()
if not vim.tbl_isempty(from_cursor) then
return from_cursor
else
return from_filetype()
end
end
local function from_filetype_load(bufnr)
return vim.split(vim.api.nvim_buf_get_option(bufnr, "filetype"), ".", true)
end
local function extend_load_ft(extend_fts)
setmetatable(extend_fts, {
-- if the filetype is not extended, only it itself should be loaded.
-- preventing ifs via __index.
__index = function(t, ft)
local val = { ft }
rawset(t, ft, val)
return val
end,
})
for ft, _ in pairs(extend_fts) do
-- append the regular filetype to the extend-filetypes.
table.insert(extend_fts[ft], ft)
end
return function(bufnr)
local fts =
vim.split(vim.api.nvim_buf_get_option(bufnr, "filetype"), ".", true)
local res = {}
for _, ft in ipairs(fts) do
vim.list_extend(res, extend_fts[ft])
end
return res
end
end
return {
from_filetype = from_filetype,
from_cursor_pos = from_cursor_pos,
from_pos_or_filetype = from_pos_or_filetype,
from_filetype_load = from_filetype_load,
extend_load_ft = extend_load_ft,
}

View File

@ -0,0 +1,231 @@
local text_node = require("luasnip.nodes.textNode").T
local wrap_nodes = require("luasnip.util.util").wrap_nodes
local extend_decorator = require("luasnip.util.extend_decorator")
local Str = require("luasnip.util.str")
local rp = require("luasnip.extras").rep
-- https://gist.github.com/tylerneylon/81333721109155b2d244
local function copy3(obj, seen)
-- Handle non-tables and previously-seen tables.
if type(obj) ~= "table" then
return obj
end
if seen and seen[obj] then
return seen[obj]
end
-- New table; mark it as seen an copy recursively.
local s = seen or {}
local res = {}
s[obj] = res
for k, v in next, obj do
res[copy3(k, s)] = copy3(v, s)
end
return setmetatable(res, getmetatable(obj))
end
-- Interpolate elements from `args` into format string with placeholders.
--
-- The placeholder syntax for selecting from `args` is similar to fmtlib and
-- Python's .format(), with some notable differences:
-- * no format options (like `{:.2f}`)
-- * 1-based indexing
-- * numbered/auto-numbered placeholders can be mixed; numbered ones set the
-- current index to new value, so following auto-numbered placeholders start
-- counting from the new value (e.g. `{} {3} {}` is `{1} {3} {4}`)
--
-- Arguments:
-- fmt: string with placeholders
-- args: table with list-like and/or map-like keys
-- opts:
-- delimiters: string, 2 distinct characters (left, right), default "{}"
-- strict: boolean, set to false to allow for unused `args`, default true
-- repeat_duplicates: boolean, repeat nodes which have jump_index instead of copying them, default false
-- Returns: a list of strings and elements of `args` inserted into placeholders
local function interpolate(fmt, args, opts)
local defaults = {
delimiters = "{}",
strict = true,
repeat_duplicates = false,
}
opts = vim.tbl_extend("force", defaults, opts or {})
-- sanitize delimiters
assert(
#opts.delimiters == 2,
'Currently only single-char delimiters are supported, e.g. delimiters="{}" (left, right)'
)
assert(
opts.delimiters:sub(1, 1) ~= opts.delimiters:sub(2, 2),
"Delimiters must be two _different_ characters"
)
local delimiters = {
left = opts.delimiters:sub(1, 1),
right = opts.delimiters:sub(2, 2),
esc_left = vim.pesc(opts.delimiters:sub(1, 1)),
esc_right = vim.pesc(opts.delimiters:sub(2, 2)),
}
-- manage insertion of text/args
local elements = {}
local last_index = 0
local used_keys = {}
local add_text = function(text)
if #text > 0 then
table.insert(elements, text)
end
end
local add_arg = function(placeholder)
local num = tonumber(placeholder)
local key
if num then -- numbered placeholder
last_index = num
key = last_index
elseif placeholder == "" then -- auto-numbered placeholder
key = last_index + 1
last_index = key
else -- named placeholder
key = placeholder
end
assert(
args[key],
string.format(
"Missing key `%s` in format arguments: `%s`",
key,
fmt
)
)
-- if the node was already used, insert a copy of it.
-- The nodes are modified in-place as part of constructing the snippet,
-- modifying one node twice will lead to UB.
if used_keys[key] then
local jump_index = args[key]:get_jump_index() -- For nodes that don't have a jump index, copy it instead
if not opts.repeat_duplicates or jump_index == nil then
table.insert(elements, copy3(args[key]))
else
table.insert(elements, rp(jump_index))
end
else
table.insert(elements, args[key])
used_keys[key] = true
end
end
-- iterate keeping a range from previous match, e.g. (not in_placeholder vs in_placeholder)
-- "Sample {2} string {3}." OR "Sample {2} string {3}."
-- left^--------^right OR left^-^right
local pattern =
string.format("[%s%s]", delimiters.esc_left, delimiters.esc_right)
local in_placeholder = false
local left = 0
while true do
local right = fmt:find(pattern, left + 1)
-- if not found, add the remaining part of string and finish
if right == nil then
assert(
not in_placeholder,
string.format('Missing closing delimiter: "%s"', fmt:sub(left))
)
add_text(fmt:sub(left + 1))
break
end
-- check if the delimiters are escaped
local delim = fmt:sub(right, right)
local next_char = fmt:sub(right + 1, right + 1)
if not in_placeholder and delim == next_char then
-- add the previous part of the string with a single delimiter
add_text(fmt:sub(left + 1, right))
-- and jump over the second one
left = right + 1
-- "continue"
else -- non-escaped delimiter
assert(
delim
== (in_placeholder and delimiters.right or delimiters.left),
string.format(
'Found unescaped %s %s placeholder; format[%d:%d]="%s"',
delim,
in_placeholder and "inside" or "outside",
left,
right,
fmt:sub(left, right)
)
)
-- add arg/text depending on current state
local add = in_placeholder and add_arg or add_text
add(fmt:sub(left + 1, right - 1))
-- update state
left = right
in_placeholder = delim == delimiters.left
end
end
-- sanity check: all arguments were used
if opts.strict then
for key, _ in pairs(args) do
assert(
used_keys[key],
string.format("Unused argument: args[%s]", key)
)
end
end
return elements
end
-- Use a format string with placeholders to interpolate nodes.
--
-- See `interpolate` documentation for details on the format.
--
-- Arguments:
-- str: format string
-- nodes: snippet node or list of nodes
-- opts: optional table
-- trim_empty: boolean, remove whitespace-only first/last lines, default true
-- dedent: boolean, remove all common indent in `str`, default true
-- ... the rest is passed to `interpolate`
-- Returns: list of snippet nodes
local function format_nodes(str, nodes, opts)
local defaults = {
trim_empty = true,
dedent = true,
}
opts = vim.tbl_extend("force", defaults, opts or {})
-- allow to pass a single node
nodes = wrap_nodes(nodes)
-- optimization: avoid splitting multiple times
local lines = nil
lines = vim.split(str, "\n", true)
Str.process_multiline(lines, opts)
str = table.concat(lines, "\n")
-- pop format_nodes's opts
for key, _ in ipairs(defaults) do
opts[key] = nil
end
local parts = interpolate(str, nodes, opts)
return vim.tbl_map(function(part)
-- wrap strings in text nodes
if type(part) == "string" then
return text_node(vim.split(part, "\n", true))
else
return part
end
end, parts)
end
extend_decorator.register(format_nodes, { arg_indx = 3 })
return {
interpolate = interpolate,
format_nodes = format_nodes,
-- alias
fmt = format_nodes,
fmta = extend_decorator.apply(format_nodes, { delimiters = "<>" }),
}

View File

@ -0,0 +1,164 @@
local F = require("luasnip.nodes.functionNode").F
local SN = require("luasnip.nodes.snippet").SN
local D = require("luasnip.nodes.dynamicNode").D
local I = require("luasnip.nodes.insertNode").I
local lambda = {}
local function _concat(lines)
return table.concat(lines, "\n")
end
local function make_lambda_args(node_args, imm_parent)
local snip = imm_parent.snippet
-- turn args' table-multilines into \n-multilines (needs to be possible
-- to process args with luas' string-functions).
local args = vim.tbl_map(_concat, node_args)
setmetatable(args, {
__index = function(table, key)
local val
-- key may be capture or env-variable.
local num = key:match("CAPTURE(%d+)")
if num then
val = snip.captures[tonumber(num)]
else
-- env may be string or table.
if type(snip.env[key]) == "table" then
-- table- to \n-multiline.
val = _concat(snip.env[key])
else
val = snip.env[key]
end
end
rawset(table, key, val)
return val
end,
})
return args
end
local function expr_to_fn(expr)
local _lambda = require("luasnip.extras._lambda")
local fn_code = _lambda.instantiate(expr)
local function fn(args, snip)
-- to be sure, lambda may end with a `match` returning nil.
local out = fn_code(make_lambda_args(args, snip)) or ""
return vim.split(out, "\n")
end
return fn
end
local LM = {}
function LM:__index(key)
return require("luasnip.extras._lambda")[key]
end
function LM:__call(expr, input_ids)
return F(expr_to_fn(expr), input_ids)
end
setmetatable(lambda, LM)
local function to_function(val, use_re)
if type(val) == "function" then
return val
end
if type(val) == "string" and not use_re then
return function()
return val
end
end
if type(val) == "string" and use_re then
return function(args)
return _concat(args[1]):match(val)
end
end
if lambda.isPE(val) then
local lmb = lambda.instantiate(val)
return function(args, snip)
return lmb(make_lambda_args(args, snip))
end
end
assert(false, "Can't convert argument to function")
end
local function match(index, _match, _then, _else)
assert(_match, "You have to pass at least 2 arguments")
_match = to_function(_match, true)
_then = to_function(_then or function(args, snip)
local match_return = _match(args, snip)
return (
(
type(match_return) == "string"
-- _assume_ table of string.
or type(match_return) == "table"
) and match_return
) or ""
end)
_else = to_function(_else or "")
local function func(args, snip)
local out = nil
if _match(args, snip) then
out = _then(args, snip)
else
out = _else(args, snip)
end
-- \n is used as a line-separator for simple strings.
return type(out) == "string" and vim.split(out, "\n") or out
end
return F(func, index)
end
return {
lambda = lambda,
match = match,
-- repeat a node.
rep = function(node_indx)
return F(function(args)
return args[1]
end, node_indx)
end,
-- Insert the output of a function.
partial = function(func, ...)
return F(function(_, _, ...)
return func(...)
end, {}, { user_args = { ... } })
end,
nonempty = function(indx, text_if, text_if_not)
assert(
type(indx) == "number",
"this only checks one node for emptiness!"
)
assert(
text_if,
"At least the text for nonemptiness has to be supplied."
)
return F(function(args)
return (args[1][1] ~= "" or #args[1] > 1) and text_if
or (text_if_not or "")
end, {
indx,
})
end,
dynamic_lambda = function(pos, lambd, args_indcs)
local insert_preset_text_func = lambda.instantiate(lambd)
return D(pos, function(args, imm_parent)
-- to be sure, lambda may end with a `match` returning nil.
local out = insert_preset_text_func(
make_lambda_args(args, imm_parent)
) or ""
return SN(pos, {
I(1, vim.split(out, "\n")),
})
end, args_indcs)
end,
--alias
l = lambda,
m = match,
}

View File

@ -0,0 +1,89 @@
local ls = require("luasnip")
local cp = require("luasnip.util.functions").copy
local p = require("luasnip.extras._parser_combinator")
local dedent = require("luasnip.util.str").dedent
local M = {}
local T = { EOL = "EOL", TXT = "TXT", INP = "INP" }
local chunk = p.any(
p.map(p.literal("$$"), function()
return { T.TXT, "$" }
end),
p.map(p.literal("\n"), function()
return { T.EOL }
end),
p.map(p.seq(p.literal("$"), p.pattern("%w*")), function(c)
return { T.INP, c[1] }
end),
p.map(p.pattern("[^\n$]*"), function(c)
return { T.TXT, c }
end)
)
M._snippet_chunks = p.star(chunk)
function M._txt_to_snip(txt)
local t = ls.t
local s = ls.s
local i = ls.i
local f = ls.f
txt = dedent(txt)
-- The parser does not handle empty strings
if txt == "" then
return s("", t({ "" }))
end
local _, chunks, _ = M._snippet_chunks(txt, 1)
local current_text_arg = { "" }
local nodes = {}
local know_inputs = {}
local last_input_pos = 0
for _, part in ipairs(chunks) do
if part[1] == T.TXT then
current_text_arg[#current_text_arg] = current_text_arg[#current_text_arg]
.. part[2]
elseif #current_text_arg > 1 or current_text_arg[1] ~= "" then
table.insert(nodes, t(current_text_arg))
current_text_arg = { "" }
end
if part[1] == T.EOL then
table.insert(current_text_arg, "")
elseif part[1] == T.INP then
local inp_pos = know_inputs[part[2]]
if inp_pos then
table.insert(nodes, f(cp, { inp_pos }))
else
last_input_pos = last_input_pos + 1
know_inputs[part[2]] = last_input_pos
table.insert(nodes, i(last_input_pos, part[2]))
end
end
end
if #current_text_arg > 1 or current_text_arg[1] ~= "" then
table.insert(nodes, t(current_text_arg))
end
return s("", nodes)
end
local last_snip = nil
local last_reg = nil
-- Create snippets On The Fly
-- It's advaisable not to use the default register as luasnip will probably
-- override it
function M.on_the_fly(regname)
regname = regname or ""
local reg = table.concat(vim.fn.getreg(regname, 1, true), "\n") -- Avoid eol in the last line
if last_reg ~= reg then
last_reg = reg
last_snip = M._txt_to_snip(reg)
end
ls.snip_expand(last_snip)
end
return M

View File

@ -0,0 +1,75 @@
local snip = require("luasnip.nodes.snippet").S
local events = require("luasnip.util.events")
local extend_decorator = require("luasnip.util.extend_decorator")
local node_util = require("luasnip.nodes.util")
local util = require("luasnip.util.util")
local matches = {
default = [[[%w%.%_%-%"%']+$]],
line = "^.+$",
}
local function wrap_resolve_expand_params(match_pattern, user_resolve)
return function(snippet, line_to_cursor, match, captures)
if line_to_cursor:sub(1, -1 - #match):match(match_pattern) == nil then
return nil
end
local pos = util.get_cursor_0ind()
local line_to_cursor_except_match =
line_to_cursor:sub(1, #line_to_cursor - #match)
local postfix_match = line_to_cursor_except_match:match(match_pattern)
or ""
local res = {
clear_region = {
from = { pos[1], pos[2] - #postfix_match - #match },
to = pos,
},
env_override = {
POSTFIX_MATCH = postfix_match,
},
}
if user_resolve then
local user_res =
user_resolve(snippet, line_to_cursor, match, captures)
if user_res then
res = vim.tbl_deep_extend("force", res, user_res, {
env_override = {},
})
else
return nil
end
end
return res
end
end
local function postfix(context, nodes, opts)
opts = opts or {}
local user_callback = vim.tbl_get(opts, "callbacks", -1, events.pre_expand)
vim.validate({
context = { context, { "string", "table" } },
nodes = { nodes, "table" },
opts = { opts, "table" },
user_callback = { user_callback, { "nil", "function" } },
})
context = node_util.wrap_context(context)
context.wordTrig = false
local match_pattern = context.match_pattern or matches.default
context.resolveExpandParams =
wrap_resolve_expand_params(match_pattern, context.resolveExpandParams)
return snip(context, nodes, opts)
end
extend_decorator.register(
postfix,
{ arg_indx = 1, extend = node_util.snippet_extend_context },
{ arg_indx = 3 }
)
return {
postfix = postfix,
matches = matches,
}

View File

@ -0,0 +1,25 @@
local session = require("luasnip.session")
local ls = require("luasnip")
local function set_choice_callback(_, indx)
if not indx then
return
end
-- feed+immediately execute i to enter INSERT after vim.ui.input closes.
vim.api.nvim_feedkeys("i", "x", false)
ls.set_choice(indx)
end
local function select_choice()
assert(
session.active_choice_nodes[vim.api.nvim_get_current_buf()],
"No active choiceNode"
)
vim.ui.select(
ls.get_current_choices(),
{ kind = "luasnip" },
set_choice_callback
)
end
return select_choice

View File

@ -0,0 +1,190 @@
local Source = require("luasnip.session.snippet_collection.source")
local util = require("luasnip.util.util")
-- stylua: ignore
local tsquery_parse =
(vim.treesitter.query and vim.treesitter.query.parse)
and vim.treesitter.query.parse
or vim.treesitter.parse_query
local M = {}
-- return: 4-tuple, {start_line, start_col, end_line, end_col}, range of
-- function-call.
local function lua_find_function_call_node_at(bufnr, line)
local has_parser, parser = pcall(vim.treesitter.get_parser, bufnr, "lua")
if not has_parser then
error("Error while getting parser: " .. parser)
end
local root = parser:parse()[1]:root()
local query = tsquery_parse("lua", [[(function_call) @f_call]])
for _, node, _ in query:iter_captures(root, bufnr, line, line + 300) do
if node:range() == line then
return { node:range() }
end
end
error(
"Query for `(function_call)` starting at line %s did not yield a result."
)
end
local function range_highlight(line_start, line_end, hl_duration_ms)
-- make sure line_end is also visible.
vim.api.nvim_win_set_cursor(0, { line_end, 0 })
vim.api.nvim_win_set_cursor(0, { line_start, 0 })
if hl_duration_ms > 0 then
local hl_buf = vim.api.nvim_get_current_buf()
-- highlight snippet for 1000ms
local id = vim.api.nvim_buf_set_extmark(
hl_buf,
ls.session.ns_id,
line_start - 1,
0,
{
-- one line below, at col 0 => entire last line is highlighted.
end_row = line_end - 1 + 1,
hl_group = "Visual",
}
)
vim.defer_fn(function()
vim.api.nvim_buf_del_extmark(hl_buf, ls.session.ns_id, id)
end, hl_duration_ms)
end
end
local function json_find_snippet_definition(bufnr, filetype, snippet_name)
local parser_ok, parser = pcall(vim.treesitter.get_parser, bufnr, filetype)
if not parser_ok then
error("Error while getting parser: " .. parser)
end
local root = parser:parse()[1]:root()
-- don't want to pass through whether this file is json or jsonc, just use
-- parser-language.
local query = tsquery_parse(
parser:lang(),
([[
(pair
key: (string (string_content) @key (#eq? @key "%s"))
) @snippet
]]):format(snippet_name)
)
for id, node, _ in query:iter_captures(root, bufnr) do
if
query.captures[id] == "snippet"
and node:parent():parent() == root
then
-- return first match.
return { node:range() }
end
end
error(
("Treesitter did not find the definition for snippet `%s`"):format(
snippet_name
)
)
end
local function win_edit(file)
vim.api.nvim_command(":e " .. file)
end
function M.jump_to_snippet(snip, opts)
opts = opts or {}
local hl_duration_ms = opts.hl_duration_ms or 1500
local edit_fn = opts.edit_fn or win_edit
local source = Source.get(snip)
if not source then
print("Snippet does not have a source.")
return
end
edit_fn(source.file)
-- assumption: after this, file is the current buffer.
if source.line and source.line_end then
-- happy path: we know both begin and end of snippet-definition.
range_highlight(source.line, source.line_end, hl_duration_ms)
return
end
local fcall_range
local ft = util.ternary(
vim.bo[0].filetype ~= "",
vim.bo[0].filetype,
vim.api.nvim_buf_get_name(0):match("%.([^%.]+)$")
)
if ft == "lua" then
if source.line then
-- in lua-file, can get region of definition via treesitter.
-- 0: current buffer.
local ok
ok, fcall_range =
pcall(lua_find_function_call_node_at, 0, source.line - 1)
if not ok then
print(
"Could not determine range for snippet-definition: "
.. fcall_range
)
vim.api.nvim_win_set_cursor(0, { source.line, 0 })
return
end
else
print("Can't jump to snippet: source does not provide line.")
return
end
-- matches *.json or *.jsonc.
elseif ft == "json" or ft == "jsonc" then
local ok
ok, fcall_range = pcall(json_find_snippet_definition, 0, ft, snip.name)
if not ok then
print(
"Could not determine range of snippet-definition: "
.. fcall_range
)
return
end
else
print(
("Don't know how to highlight snippet-definitions in current buffer `%s`.%s"):format(
vim.api.nvim_buf_get_name(0),
source.line ~= nil and " Jumping to `source.line`" or ""
)
)
if source.line ~= nil then
vim.api.nvim_win_set_cursor(0, { source.line, 0 })
end
return
end
assert(fcall_range ~= nil, "fcall_range is not nil")
-- 1 is line_from, 3 is line_end.
-- +1 since range is row-0-indexed.
range_highlight(fcall_range[1] + 1, fcall_range[3] + 1, hl_duration_ms)
local new_source = Source.from_location(
source.file,
{ line = fcall_range[1] + 1, line_end = fcall_range[3] + 1 }
)
Source.set(snip, new_source)
end
function M.jump_to_active_snippet(opts)
local active_node =
require("luasnip.session").current_nodes[vim.api.nvim_get_current_buf()]
if not active_node then
print("No active snippet.")
return
end
local snip = active_node.parent.snippet
M.jump_to_snippet(snip, opts)
end
return M

View File

@ -0,0 +1,96 @@
local available = require("luasnip").available
local function snip_info(snippet)
return {
name = snippet.name,
trigger = snippet.trigger,
description = snippet.description,
wordTrig = snippet.wordTrig and true or false,
regTrig = snippet.regTrig and true or false,
docstring = snippet:get_docstring(),
}
end
local function get_name(buf)
return "LuaSnip://Snippets"
end
local win_opts = { foldmethod = "indent" }
local buf_opts = { filetype = "lua" }
local function set_win_opts(win, opts)
for opt, val in pairs(opts) do
vim.api.nvim_win_set_option(win, opt, val)
end
end
local function set_buf_opts(buf, opts)
for opt, val in pairs(opts) do
vim.api.nvim_buf_set_option(buf, opt, val)
end
end
local function make_scratch_buf(buf)
local opts = {
buftype = "nofile",
bufhidden = "wipe",
buflisted = false,
swapfile = false,
modified = false,
modeline = false,
}
set_buf_opts(buf, opts)
end
local function display_split(opts)
opts = opts or {}
opts.win_opts = opts.win_opts or win_opts
opts.buf_opts = opts.buf_opts or buf_opts
opts.get_name = opts.get_name or get_name
return function(printer_result)
-- create and open buffer on 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()
-- make scratch buffer
vim.api.nvim_buf_set_name(buf, opts.get_name(buf))
make_scratch_buf(buf)
-- disable diagnostics
vim.diagnostic.disable(buf)
-- set any extra win and buf opts
set_win_opts(win, opts.win_opts)
set_buf_opts(buf, opts.buf_opts)
-- dump snippets
local replacement = vim.split(printer_result, "\n")
vim.api.nvim_buf_set_lines(buf, 0, 0, false, replacement)
-- make it unmodifiable at this point
vim.api.nvim_buf_set_option(buf, "modifiable", false)
end
end
local function open(opts)
opts = opts or {}
opts.snip_info = opts.snip_info or snip_info
opts.printer = opts.printer or vim.inspect
opts.display = opts.display or display_split()
-- load snippets before changing windows/buffers
local snippets = available(opts.snip_info)
-- open snippets
opts.display(opts.printer(snippets))
end
return {
open = open,
options = { display = display_split },
}

View File

@ -0,0 +1,324 @@
if vim.version().major == 0 and vim.version().minor < 9 then
-- need LanguageTree:tree_for_range and don't want to go through the hassle
-- of differentiating multiple version of query.get/parse.
error("treesitter_postfix does not support neovim < 0.9")
end
local snip = require("luasnip.nodes.snippet").S
local ts = require("luasnip.extras._treesitter")
local node_util = require("luasnip.nodes.util")
local extend_decorator = require("luasnip.util.extend_decorator")
local tbl = require("luasnip.util.table")
local util = require("luasnip.util.util")
--- Normalize the arguments passed to treesitter_postfix into a function that
--- returns treesitter-matches to the specified query+captures.
---@param opts LuaSnip.extra.MatchTSNodeOpts
---@return LuaSnip.extra.MatchTSNodeFunc
local function generate_match_tsnode_func(opts)
local match_opts = {}
if opts.query then
match_opts.query =
vim.treesitter.query.parse(opts.query_lang, opts.query)
else
match_opts.query = vim.treesitter.query.get(
opts.query_lang,
opts.query_name or "luasnip"
)
end
match_opts.generator = ts.captures_iter(opts.match_captures or "prefix")
if type(opts.select) == "function" then
match_opts.selector = opts.select
elseif type(opts.select) == "string" then
match_opts.selector = ts.builtin_tsnode_selectors[opts.select]
assert(
match_opts.selector,
"Selector " .. opts.select .. "is not known"
)
else
match_opts.selector = ts.builtin_tsnode_selectors.any
end
---@param parser LuaSnip.extra.TSParser
---@param pos { [1]: number, [2]: number }
return function(parser, pos)
return parser:match_at(
match_opts, --[[@as LuaSnip.extra.MatchTSNodeOpts]]
pos
)
end
end
local function make_reparse_enter_and_leave_func(
reparse,
bufnr,
trigger_region,
trigger
)
if reparse == "live" then
local context = ts.FixBufferContext.new(bufnr, trigger_region, trigger)
return function()
return context:enter()
end, function(_)
context:leave()
end
elseif reparse == "copy" then
local parser, source =
ts.reparse_buffer_after_removing_match(bufnr, trigger_region)
return function()
return parser, source
end, function()
parser:destroy()
end
else
return function()
return vim.treesitter.get_parser(bufnr), bufnr
end, function(_) end
end
end
---Optionally parse the buffer
---@param reparse boolean|string|nil
---@param real_resolver function
---@return fun(snippet, line_to_cursor, matched_trigger, captures):table?
local function wrap_with_reparse_context(reparse, real_resolver)
return function(snippet, line_to_cursor, matched_trigger, captures)
local bufnr = vim.api.nvim_win_get_buf(0)
local cursor = util.get_cursor_0ind()
local trigger_region = {
row = cursor[1],
col_range = {
-- includes from, excludes to.
cursor[2] - #matched_trigger,
cursor[2],
},
}
local enter, leave = make_reparse_enter_and_leave_func(
reparse,
bufnr,
trigger_region,
matched_trigger
)
local parser, source = enter()
if parser == nil or source == nil then
return nil
end
local ret = real_resolver(
snippet,
line_to_cursor,
matched_trigger,
captures,
parser,
source,
bufnr,
{ cursor[1], cursor[2] - #matched_trigger }
)
leave()
return ret
end
end
---@param match_tsnode LuaSnip.extra.MatchTSNodeFunc Determines the constraints on the matched node.
local function generate_resolve_expand_param(match_tsnode, user_resolver)
---@param snippet any
---@param line_to_cursor string
---@param matched_trigger string
---@param captures any
---@param parser LanguageTree
---@param source number|string
---@param bufnr number
---@param pos { [1]: number, [2]: number }
return function(
snippet,
line_to_cursor,
matched_trigger,
captures,
parser,
source,
bufnr,
pos
)
local ts_parser = ts.TSParser.new(bufnr, parser, source)
if ts_parser == nil then
return
end
local row, col = unpack(pos)
local best_match, prefix_node = match_tsnode(ts_parser, { row, col })
if best_match == nil or prefix_node == nil then
return nil
end
local start_row, start_col, _, _ = prefix_node:range()
local env = {
LS_TSMATCH = vim.split(ts_parser:get_node_text(prefix_node), "\n"),
-- filled subsequently.
LS_TSDATA = {},
}
for capture_name, node in pairs(best_match) do
env["LS_TSCAPTURE_" .. capture_name:upper()] =
vim.split(ts_parser:get_node_text(node), "\n")
local from_r, from_c, to_r, to_c = node:range()
env.LS_TSDATA[capture_name] = {
type = node:type(),
range = { { from_r, from_c }, { to_r, to_c } },
}
end
local ret = {
trigger = matched_trigger,
captures = captures,
clear_region = {
from = {
start_row,
start_col,
},
to = {
pos[1],
pos[2] + #matched_trigger,
},
},
env_override = env,
}
if user_resolver then
local user_res = user_resolver(
snippet,
line_to_cursor,
matched_trigger,
captures
)
if user_res then
ret = vim.tbl_deep_extend(
"force",
ret,
user_res,
{ env_override = {} }
)
else
return nil
end
end
return ret
end
end
local function generate_simple_parent_lookup_function(lookup_fun)
---@param types string|string[]
---@return LuaSnip.extra.MatchTSNodeFunc
return function(types)
local type_checker = tbl.list_to_set(types)
---@param parser LuaSnip.extra.TSParser
---@param pos { [1]: number, [2]: number }
return function(parser, pos)
-- check node just before the position.
local root = parser:get_node_at_pos({ pos[1], pos[2] - 1 })
if root == nil then
return
end
---@param node TSNode
local check_node_exclude_pos = function(node)
local _, _, end_row, end_col = node:range(false)
return end_row == pos[1] and end_col == pos[2]
end
---@param node TSNode
local check_node_type = function(node)
return type_checker[node:type()]
end
local prefix_node = lookup_fun(root, function(node)
return check_node_type(node) and check_node_exclude_pos(node)
end)
if prefix_node == nil then
return nil, nil
end
return {}, prefix_node
end
end
end
---@param n number
local function find_nth_parent(n)
---@param parser LuaSnip.extra.TSParser
---@param pos { [1]: number, [2]: number }
return function(parser, pos)
local inner_node = parser:get_node_at_pos({ pos[1], pos[2] - 1 })
if inner_node == nil then
return
end
---@param node TSNode
local check_node_exclude_pos = function(node)
local _, _, end_row, end_col = node:range(false)
return end_row == pos[1] and end_col == pos[2]
end
return {}, ts.find_nth_parent(inner_node, n, check_node_exclude_pos)
end
end
local function treesitter_postfix(context, nodes, opts)
opts = opts or {}
vim.validate({
context = { context, { "string", "table" } },
nodes = { nodes, "table" },
opts = { opts, "table" },
})
context = node_util.wrap_context(context)
context.wordTrig = false
---@type LuaSnip.extra.MatchTSNodeFunc
local match_tsnode_func
if type(context.matchTSNode) == "function" then
match_tsnode_func = context.matchTSNode
else
match_tsnode_func = generate_match_tsnode_func(context.matchTSNode)
end
local expand_params_resolver = generate_resolve_expand_param(
match_tsnode_func,
context.resolveExpandParams
)
context.resolveExpandParams =
wrap_with_reparse_context(context.reparseBuffer, expand_params_resolver)
return snip(context, nodes, opts)
end
extend_decorator.register(
treesitter_postfix,
{ arg_indx = 1, extend = node_util.snippet_extend_context },
{ arg_indx = 3 }
)
return {
treesitter_postfix = treesitter_postfix,
builtin = {
tsnode_matcher = {
find_topmost_types = generate_simple_parent_lookup_function(
ts.find_topmost_parent
),
find_first_types = generate_simple_parent_lookup_function(
ts.find_first_parent
),
find_nth_parent = find_nth_parent,
},
},
}

View File

@ -0,0 +1,14 @@
return {
check = function()
vim.health.start("luasnip")
local jsregexp = require("luasnip.util.jsregexp")
if jsregexp then
vim.health.ok("jsregexp is installed")
else
vim.health.warn([[
For Variable/Placeholder-transformations, luasnip requires
the jsregexp library. See `:h luasnip-lsp-snippets-transformations` for advice
]])
end
end,
}

View File

@ -0,0 +1,896 @@
local util = require("luasnip.util.util")
local lazy_table = require("luasnip.util.lazy_table")
local types = require("luasnip.util.types")
local node_util = require("luasnip.nodes.util")
local session = require("luasnip.session")
local snippet_collection = require("luasnip.session.snippet_collection")
local Environ = require("luasnip.util.environ")
local extend_decorator = require("luasnip.util.extend_decorator")
local loader = require("luasnip.loaders")
local next_expand = nil
local next_expand_params = nil
local ls
local luasnip_data_dir = vim.fn.stdpath("cache") .. "/luasnip"
local log = require("luasnip.util.log").new("main")
local function get_active_snip()
local node = session.current_nodes[vim.api.nvim_get_current_buf()]
if not node then
return nil
end
while node.parent do
node = node.parent
end
return node
end
-- returns matching snippet (needs to be copied before usage!) and its expand-
-- parameters(trigger and captures). params are returned here because there's
-- no need to recalculate them.
local function match_snippet(line, type)
return snippet_collection.match_snippet(
line,
util.get_snippet_filetypes(),
type
)
end
-- ft:
-- * string: interpreted as filetype, return corresponding snippets.
-- * nil: return snippets for all filetypes:
-- {
-- lua = {...},
-- cpp = {...},
-- ...
-- }
-- opts: optional args, can contain `type`, either "snippets" or "autosnippets".
--
-- return table, may be empty.
local function get_snippets(ft, opts)
opts = opts or {}
return snippet_collection.get_snippets(ft, opts.type or "snippets") or {}
end
local function default_snip_info(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
local function available(snip_info)
snip_info = snip_info or default_snip_info
local fts = util.get_snippet_filetypes()
local res = {}
for _, ft in ipairs(fts) do
res[ft] = {}
for _, snip in ipairs(get_snippets(ft)) do
if not snip.invalidated then
table.insert(res[ft], snip_info(snip))
end
end
for _, snip in ipairs(get_snippets(ft, { type = "autosnippets" })) do
if not snip.invalidated then
table.insert(res[ft], snip_info(snip))
end
end
end
return res
end
local unlink_set_adjacent_as_current
local function unlink_set_adjacent_as_current_no_log(snippet)
-- prefer setting previous/outer insertNode as current node.
local next_current =
-- either pick i0 of snippet before, or i(-1) of next snippet.
snippet.prev.prev or snippet:next_node()
snippet:remove_from_jumplist()
if next_current then
-- if snippet was active before, we need to now set its parent to be no
-- longer inner_active.
if
snippet.parent_node == next_current and next_current.inner_active
then
snippet.parent_node:input_leave_children()
else
-- set no_move.
local ok, err = pcall(next_current.input_enter, next_current, true)
if not ok then
-- this won't try to set the previously broken snippet as
-- current, since that link is removed in
-- `remove_from_jumplist`.
unlink_set_adjacent_as_current(
next_current.parent.snippet,
"Error while setting adjacent snippet as current node: %s",
err
)
end
end
end
session.current_nodes[vim.api.nvim_get_current_buf()] = next_current
end
function unlink_set_adjacent_as_current(snippet, reason, ...)
log.warn("Removing snippet %s: %s", snippet.trigger, reason:format(...))
unlink_set_adjacent_as_current_no_log(snippet)
end
local function unlink_current()
local current = session.current_nodes[vim.api.nvim_get_current_buf()]
if not current then
print("No active Snippet")
return
end
unlink_set_adjacent_as_current_no_log(current.parent.snippet)
end
-- return next active node.
local function safe_jump_current(dir, no_move, dry_run)
local node = session.current_nodes[vim.api.nvim_get_current_buf()]
if not node then
return nil
end
local ok, res = pcall(node.jump_from, node, dir, no_move, dry_run)
if ok then
return res
else
local snip = node.parent.snippet
unlink_set_adjacent_as_current(
snip,
"Removing snippet `%s` due to error %s",
snip.trigger,
res
)
return session.current_nodes[vim.api.nvim_get_current_buf()]
end
end
local function jump(dir)
local current = session.current_nodes[vim.api.nvim_get_current_buf()]
if current then
local next_node = util.no_region_check_wrap(safe_jump_current, dir)
if next_node == nil then
session.current_nodes[vim.api.nvim_get_current_buf()] = nil
return true
end
if session.config.exit_roots then
if next_node.pos == 0 and next_node.parent.parent_node == nil then
session.current_nodes[vim.api.nvim_get_current_buf()] = nil
return true
end
end
session.current_nodes[vim.api.nvim_get_current_buf()] = next_node
return true
else
return false
end
end
local function jump_destination(dir)
-- dry run of jump (+no_move ofc.), only retrieves destination-node.
return safe_jump_current(dir, true, { active = {} })
end
local function jumpable(dir)
-- node is jumpable if there is a destination.
return jump_destination(dir)
~= session.current_nodes[vim.api.nvim_get_current_buf()]
end
local function expandable()
next_expand, next_expand_params =
match_snippet(util.get_current_line_to_cursor(), "snippets")
return next_expand ~= nil
end
local function expand_or_jumpable()
return expandable() or jumpable(1)
end
local function in_snippet()
-- check if the cursor on a row inside a snippet.
local node = session.current_nodes[vim.api.nvim_get_current_buf()]
if not node then
return false
end
local snippet = node.parent.snippet
local ok, snip_begin_pos, snip_end_pos =
pcall(snippet.mark.pos_begin_end, snippet.mark)
if not ok then
-- if there was an error getting the position, the snippets text was
-- most likely removed, resulting in messed up extmarks -> error.
-- remove the snippet.
unlink_set_adjacent_as_current(
snippet,
"Error while getting extmark-position: %s",
snip_begin_pos
)
return
end
local pos = vim.api.nvim_win_get_cursor(0)
if pos[1] - 1 >= snip_begin_pos[1] and pos[1] - 1 <= snip_end_pos[1] then
return true -- cursor not on row inside snippet
end
end
local function expand_or_locally_jumpable()
return expandable() or (in_snippet() and jumpable(1))
end
local function locally_jumpable(dir)
return in_snippet() and jumpable(dir)
end
local function _jump_into_default(snippet)
return util.no_region_check_wrap(snippet.jump_into, snippet, 1)
end
-- opts.clear_region: table, keys `from` and `to`, both (0,0)-indexed.
local function snip_expand(snippet, opts)
local snip = snippet:copy()
opts = opts or {}
opts.expand_params = opts.expand_params or {}
-- override with current position if none given.
opts.pos = opts.pos or util.get_cursor_0ind()
opts.jump_into_func = opts.jump_into_func or _jump_into_default
opts.indent = vim.F.if_nil(opts.indent, true)
snip.trigger = opts.expand_params.trigger or snip.trigger
snip.captures = opts.expand_params.captures or {}
local info =
{ trigger = snip.trigger, captures = snip.captures, pos = opts.pos }
local env = Environ:new(info)
Environ:override(env, opts.expand_params.env_override or {})
local pos_id = vim.api.nvim_buf_set_extmark(
0,
session.ns_id,
opts.pos[1],
opts.pos[2],
-- track position between pos[2]-1 and pos[2].
{ right_gravity = false }
)
-- optionally clear text. Text has to be cleared befor jumping into the new
-- snippet, as the cursor-position can end up in the wrong position (to be
-- precise the text will be moved, the cursor will stay at the same
-- position, which is just as bad) if text before the cursor, on the same
-- line is cleared.
if opts.clear_region then
vim.api.nvim_buf_set_text(
0,
opts.clear_region.from[1],
opts.clear_region.from[2],
opts.clear_region.to[1],
opts.clear_region.to[2],
{ "" }
)
end
local snip_parent_node = snip:trigger_expand(
session.current_nodes[vim.api.nvim_get_current_buf()],
pos_id,
env,
opts.indent
)
-- jump_into-callback returns new active node.
session.current_nodes[vim.api.nvim_get_current_buf()] =
opts.jump_into_func(snip)
local buf_snippet_roots =
session.snippet_roots[vim.api.nvim_get_current_buf()]
if not session.config.keep_roots and #buf_snippet_roots > 1 then
-- if history is not set, and there is more than one snippet-root,
-- remove the other one.
-- The nice thing is: since we maintain that #buf_snippet_roots == 1
-- whenever outside of this function, we know that if we're here, it's
-- because this snippet was just inserted into buf_snippet_roots.
-- Armed with this knowledge, we can just check which of the roots is
-- this snippet, and remove the other one.
buf_snippet_roots[buf_snippet_roots[1] == snip and 2 or 1]:remove_from_jumplist()
end
-- stores original snippet, it doesn't contain any data from expansion.
session.last_expand_snip = snippet
session.last_expand_opts = opts
-- set last action for vim-repeat.
-- will silently fail if vim-repeat isn't available.
-- -1 to disable count.
vim.cmd([[silent! call repeat#set("\<Plug>luasnip-expand-repeat", -1)]])
return snip
end
---Find a snippet matching the current cursor-position.
---@param opts table: may contain:
--- - `jump_into_func`: passed through to `snip_expand`.
---@return boolean: whether a snippet was expanded.
local function expand(opts)
local expand_params
local snip
-- find snip via next_expand (set from previous expandable()) or manual matching.
if next_expand ~= nil then
snip = next_expand
expand_params = next_expand_params
next_expand = nil
next_expand_params = nil
else
snip, expand_params =
match_snippet(util.get_current_line_to_cursor(), "snippets")
end
if snip then
local jump_into_func = opts and opts.jump_into_func
local cursor = util.get_cursor_0ind()
local clear_region = expand_params.clear_region
or {
from = {
cursor[1],
cursor[2] - #expand_params.trigger,
},
to = cursor,
}
-- override snip with expanded copy.
snip = snip_expand(snip, {
expand_params = expand_params,
-- clear trigger-text.
clear_region = clear_region,
jump_into_func = jump_into_func,
})
return true
end
return false
end
local function expand_auto()
local snip, expand_params =
match_snippet(util.get_current_line_to_cursor(), "autosnippets")
if snip then
local cursor = util.get_cursor_0ind()
local clear_region = expand_params.clear_region
or {
from = {
cursor[1],
cursor[2] - #expand_params.trigger,
},
to = cursor,
}
snip = snip_expand(snip, {
expand_params = expand_params,
-- clear trigger-text.
clear_region = clear_region,
})
end
end
local function expand_repeat()
-- prevent clearing text with repeated expand.
session.last_expand_opts.clear_region = nil
session.last_expand_opts.pos = nil
snip_expand(session.last_expand_snip, session.last_expand_opts)
end
-- return true and expand snippet if expandable, return false if not.
local function expand_or_jump()
if expand() then
return true
end
if jump(1) then
return true
end
return false
end
local function lsp_expand(body, opts)
-- expand snippet as-is.
snip_expand(
ls.parser.parse_snippet(
"",
body,
{ trim_empty = false, dedent = false }
),
opts
)
end
local function choice_active()
return session.active_choice_nodes[vim.api.nvim_get_current_buf()] ~= nil
end
-- attempts to do some action on the snippet (like change_choice, set_choice),
-- if it fails the snippet is removed and the next snippet becomes the current node.
-- ... is passed to pcall as-is.
local function safe_choice_action(snip, ...)
local ok, res = pcall(...)
if ok then
return res
else
-- not very elegant, but this way we don't have a near
-- re-implementation of unlink_current.
unlink_set_adjacent_as_current(
snip,
"Removing snippet `%s` due to error %s",
snip.trigger,
res
)
return session.current_nodes[vim.api.nvim_get_current_buf()]
end
end
local function change_choice(val)
local active_choice =
session.active_choice_nodes[vim.api.nvim_get_current_buf()]
assert(active_choice, "No active choiceNode")
local new_active = util.no_region_check_wrap(
safe_choice_action,
active_choice.parent.snippet,
active_choice.change_choice,
active_choice,
val,
session.current_nodes[vim.api.nvim_get_current_buf()]
)
session.current_nodes[vim.api.nvim_get_current_buf()] = new_active
end
local function set_choice(choice_indx)
local active_choice =
session.active_choice_nodes[vim.api.nvim_get_current_buf()]
assert(active_choice, "No active choiceNode")
local choice = active_choice.choices[choice_indx]
assert(choice, "Invalid Choice")
local new_active = util.no_region_check_wrap(
safe_choice_action,
active_choice.parent.snippet,
active_choice.set_choice,
active_choice,
choice,
session.current_nodes[vim.api.nvim_get_current_buf()]
)
session.current_nodes[vim.api.nvim_get_current_buf()] = new_active
end
local function get_current_choices()
local active_choice =
session.active_choice_nodes[vim.api.nvim_get_current_buf()]
assert(active_choice, "No active choiceNode")
local choice_lines = {}
active_choice:update_static_all()
for i, choice in ipairs(active_choice.choices) do
choice_lines[i] = table.concat(choice:get_docstring(), "\n")
end
return choice_lines
end
local function active_update_dependents()
local active = session.current_nodes[vim.api.nvim_get_current_buf()]
-- special case for startNode, cannot focus on those (and they can't
-- have dependents)
-- don't update if a jump/change_choice is in progress.
if not session.jump_active and active and active.pos > 0 then
-- Save cursor-pos to restore later.
local cur = util.get_cursor_0ind()
local cur_mark = vim.api.nvim_buf_set_extmark(
0,
session.ns_id,
cur[1],
cur[2],
{ right_gravity = false }
)
local ok, err = pcall(active.update_dependents, active)
if not ok then
unlink_set_adjacent_as_current(
active.parent.snippet,
"Error while updating dependents for snippet %s due to error %s",
active.parent.snippet.trigger,
err
)
return
end
-- 'restore' orientation of extmarks, may have been changed by some set_text or similar.
ok, err = pcall(active.focus, active)
if not ok then
unlink_set_adjacent_as_current(
active.parent.snippet,
"Error while entering node in snippet %s: %s",
active.parent.snippet.trigger,
err
)
return
end
-- Don't account for utf, nvim_win_set_cursor doesn't either.
cur = vim.api.nvim_buf_get_extmark_by_id(
0,
session.ns_id,
cur_mark,
{ details = false }
)
util.set_cursor_0ind(cur)
end
end
local function store_snippet_docstrings(snippet_table)
-- ensure the directory exists.
-- 493 = 0755
vim.loop.fs_mkdir(luasnip_data_dir, 493)
-- fs_open() with w+ creates the file if nonexistent.
local docstring_cache_fd = vim.loop.fs_open(
luasnip_data_dir .. "/docstrings.json",
"w+",
-- 420 = 0644
420
)
-- get size for fs_read()
local cache_size = vim.loop.fs_fstat(docstring_cache_fd).size
local file_could_be_read, docstrings = pcall(
util.json_decode,
-- offset 0.
vim.loop.fs_read(docstring_cache_fd, cache_size, 0)
)
docstrings = file_could_be_read and docstrings or {}
for ft, snippets in pairs(snippet_table) do
if not docstrings[ft] then
docstrings[ft] = {}
end
for _, snippet in ipairs(snippets) do
docstrings[ft][snippet.trigger] = snippet:get_docstring()
end
end
vim.loop.fs_write(docstring_cache_fd, util.json_encode(docstrings))
end
local function load_snippet_docstrings(snippet_table)
-- ensure the directory exists.
-- 493 = 0755
vim.loop.fs_mkdir(luasnip_data_dir, 493)
-- fs_open() with "r" returns nil if the file doesn't exist.
local docstring_cache_fd = vim.loop.fs_open(
luasnip_data_dir .. "/docstrings.json",
"r",
-- 420 = 0644
420
)
if not docstring_cache_fd then
error("Cached docstrings could not be read!")
return
end
-- get size for fs_read()
local cache_size = vim.loop.fs_fstat(docstring_cache_fd).size
local docstrings = util.json_decode(
-- offset 0.
vim.loop.fs_read(docstring_cache_fd, cache_size, 0)
)
for ft, snippets in pairs(snippet_table) do
-- skip if fieltype not in cache.
if docstrings[ft] then
for _, snippet in ipairs(snippets) do
-- only set if it hasn't been set already.
if not snippet.docstring then
snippet.docstring = docstrings[ft][snippet.trigger]
end
end
end
end
end
local function unlink_current_if_deleted()
local node = session.current_nodes[vim.api.nvim_get_current_buf()]
if not node then
return
end
local snippet = node.parent.snippet
-- extmarks_valid checks that
-- * textnodes that should contain text still do so, and
-- * that extmarks still fulfill all expectations (should be successive, no gaps, etc.)
if not snippet:extmarks_valid() then
unlink_set_adjacent_as_current(
snippet,
"Detected deletion of snippet `%s`, removing it",
snippet.trigger
)
end
end
local function exit_out_of_region(node)
-- if currently jumping via luasnip or no active node:
if session.jump_active or not node then
return
end
local pos = util.get_cursor_0ind()
local snippet
if node.type == types.snippet then
snippet = node
else
snippet = node.parent.snippet
end
-- find root-snippet.
while snippet.parent_node do
snippet = snippet.parent_node.parent.snippet
end
local ok, snip_begin_pos, snip_end_pos =
pcall(snippet.mark.pos_begin_end, snippet.mark)
if not ok then
unlink_set_adjacent_as_current(
snippet,
"Error while getting extmark-position: %s",
snip_begin_pos
)
return
end
-- stylua: ignore
-- leave if curser before or behind snippet
if pos[1] < snip_begin_pos[1] or
pos[1] > snip_end_pos[1] then
-- make sure the snippet can safely be entered, since it may have to
-- be, in `refocus`.
if not snippet:extmarks_valid() then
unlink_set_adjacent_as_current(snippet, "Leaving snippet-root due to invalid extmarks.")
return
end
local next_active = snippet.insert_nodes[0]
-- if there is a snippet nested into the $0, enter its $0 instead,
-- recursively.
-- This is to ensure that a jump forward after leaving the region of a
-- root will jump to the next root, or not result in a jump at all.
while next_active.inner_first do
-- make sure next_active is nested into completely intact
-- snippets, since that is a precondition on the to-node of
if not next_active.inner_first:extmarks_valid() then
next_active.inner_first:remove_from_jumplist()
else
-- inner_first is always the snippet, not the -1-node.
next_active = next_active.inner_first.insert_nodes[0]
end
end
node_util.refocus(node, next_active)
session.current_nodes[vim.api.nvim_get_current_buf()] = next_active
end
end
-- ft string, extend_ft table of strings.
local function filetype_extend(ft, extend_ft)
vim.list_extend(session.ft_redirect[ft], extend_ft)
session.ft_redirect[ft] = util.deduplicate(session.ft_redirect[ft])
end
-- ft string, fts table of strings.
local function filetype_set(ft, fts)
session.ft_redirect[ft] = util.deduplicate(fts)
end
local function cleanup()
-- Use this to reload luasnip
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "LuasnipCleanup", modeline = false }
)
-- clear all snippets.
snippet_collection.clear_snippets()
loader.cleanup()
end
local function refresh_notify(ft)
snippet_collection.refresh_notify(ft)
end
local function setup_snip_env()
local combined_table = vim.tbl_extend("force", _G, session.config.snip_env)
-- TODO: if desired, take into account _G's __index before looking into
-- snip_env's __index.
setmetatable(combined_table, getmetatable(session.config.snip_env))
setfenv(2, combined_table)
end
local function get_snip_env()
return session.get_snip_env()
end
local function get_id_snippet(id)
return snippet_collection.get_id_snippet(id)
end
local function add_snippets(ft, snippets, opts)
-- don't use yet, not available in some neovim-versions.
--
-- vim.validate({
-- filetype = { ft, { "string", "nil" } },
-- snippets = { snippets, "table" },
-- opts = { opts, { "table", "nil" } },
-- })
opts = opts or {}
opts.refresh_notify = opts.refresh_notify or true
-- alternatively, "autosnippets"
opts.type = opts.type or "snippets"
-- if ft is nil, snippets already has this format.
if ft then
snippets = {
[ft] = snippets,
}
end
snippet_collection.add_snippets(snippets, opts)
if opts.refresh_notify then
for ft_, _ in pairs(snippets) do
refresh_notify(ft_)
end
end
end
local function clean_invalidated(opts)
opts = opts or {}
snippet_collection.clean_invalidated(opts)
end
local function activate_node(opts)
opts = opts or {}
local pos = opts.pos or util.get_cursor_0ind()
local strict = vim.F.if_nil(opts.strict, false)
local select = vim.F.if_nil(opts.select, true)
-- find tree-node the snippet should be inserted at (could be before another node).
local _, _, _, node = node_util.snippettree_find_undamaged_node(pos, {
tree_respect_rgravs = false,
tree_preference = node_util.binarysearch_preference.inside,
snippet_mode = "interactive",
})
if not node then
error("No Snippet at that position")
return
end
-- only activate interactive nodes, or nodes that are immediately nested
-- inside a choiceNode.
if not node:interactive() then
if strict then
error("Refusing to activate a non-interactive node.")
return
else
-- fall back to known insertNode.
-- snippet.insert_nodes[1] may be preferable, but that is not
-- certainly an insertNode (and does not even certainly contain an
-- insertNode, think snippetNode with only textNode).
-- We could *almost* find the first activateable node by
-- dry_run-jumping into the snippet, but then we'd also need some
-- mechanism for setting the active-state of all nodes to false,
-- which we don't yet have.
--
-- Instead, just choose -1-node, and allow jumps from there, which
-- is much simpler.
node = node.parent.snippet.prev
end
end
node_util.refocus(
session.current_nodes[vim.api.nvim_get_current_buf()],
node
)
if select then
-- input_enter node again, to get highlight and the like.
-- One side-effect of this is that an event will be execute twice, but I
-- feel like that is a trade-off worth doing, since it otherwise refocus
-- would have to be more complicated (or at least, restructured).
node:input_enter()
end
session.current_nodes[vim.api.nvim_get_current_buf()] = node
end
-- make these lazy, such that we don't have to load them before it's really
-- necessary (drives up cost of initial load, otherwise).
-- stylua: ignore
local ls_lazy = {
s = function() return require("luasnip.nodes.snippet").S end,
sn = function() return require("luasnip.nodes.snippet").SN end,
t = function() return require("luasnip.nodes.textNode").T end,
f = function() return require("luasnip.nodes.functionNode").F end,
i = function() return require("luasnip.nodes.insertNode").I end,
c = function() return require("luasnip.nodes.choiceNode").C end,
d = function() return require("luasnip.nodes.dynamicNode").D end,
r = function() return require("luasnip.nodes.restoreNode").R end,
snippet = function() return require("luasnip.nodes.snippet").S end,
snippet_node = function() return require("luasnip.nodes.snippet").SN end,
parent_indexer = function() return require("luasnip.nodes.snippet").P end,
indent_snippet_node = function() return require("luasnip.nodes.snippet").ISN end,
text_node = function() return require("luasnip.nodes.textNode").T end,
function_node = function() return require("luasnip.nodes.functionNode").F end,
insert_node = function() return require("luasnip.nodes.insertNode").I end,
choice_node = function() return require("luasnip.nodes.choiceNode").C end,
dynamic_node = function() return require("luasnip.nodes.dynamicNode").D end,
restore_node = function() return require("luasnip.nodes.restoreNode").R end,
parser = function() return require("luasnip.util.parser") end,
config = function() return require("luasnip.config") end,
multi_snippet = function() return require("luasnip.nodes.multiSnippet").new_multisnippet end,
snippet_source = function() return require("luasnip.session.snippet_collection.source") end,
select_keys = function() return require("luasnip.util.select").select_keys end
}
ls = lazy_table({
expand_or_jumpable = expand_or_jumpable,
expand_or_locally_jumpable = expand_or_locally_jumpable,
locally_jumpable = locally_jumpable,
jumpable = jumpable,
expandable = expandable,
in_snippet = in_snippet,
expand = expand,
snip_expand = snip_expand,
expand_repeat = expand_repeat,
expand_auto = expand_auto,
expand_or_jump = expand_or_jump,
jump = jump,
get_active_snip = get_active_snip,
choice_active = choice_active,
change_choice = change_choice,
set_choice = set_choice,
get_current_choices = get_current_choices,
unlink_current = unlink_current,
lsp_expand = lsp_expand,
active_update_dependents = active_update_dependents,
available = available,
exit_out_of_region = exit_out_of_region,
load_snippet_docstrings = load_snippet_docstrings,
store_snippet_docstrings = store_snippet_docstrings,
unlink_current_if_deleted = unlink_current_if_deleted,
filetype_extend = filetype_extend,
filetype_set = filetype_set,
add_snippets = add_snippets,
get_snippets = get_snippets,
get_id_snippet = get_id_snippet,
setup_snip_env = setup_snip_env,
get_snip_env = get_snip_env,
clean_invalidated = clean_invalidated,
get_snippet_filetypes = util.get_snippet_filetypes,
jump_destination = jump_destination,
session = session,
cleanup = cleanup,
refresh_notify = refresh_notify,
env_namespace = Environ.env_namespace,
setup = require("luasnip.config").setup,
extend_decorator = extend_decorator,
log = require("luasnip.util.log"),
activate_node = activate_node,
}, ls_lazy)
return ls

View File

@ -0,0 +1,27 @@
--- This module stores all files loaded by any of the loaders, ordered by their
--- filetype, and other data.
--- This is to facilitate luasnip.loaders.edit_snippets, and to handle
--- persistency of data, which is not given if it is stored in the module-file,
--- since the module-name we use (luasnip.loaders.*) is not necessarily the one
--- used by the user (luasnip/loader/*, for example), and the returned modules
--- are different tables.
local autotable = require("luasnip.util.auto_table").autotable
local M = {
lua_collections = {},
lua_ft_paths = autotable(2),
snipmate_collections = {},
snipmate_ft_paths = autotable(2),
-- set by loader.
snipmate_cache = nil,
vscode_package_collections = {},
vscode_standalone_watchers = {},
vscode_ft_paths = autotable(2),
-- set by loader.
vscode_cache = nil,
}
return M

View File

@ -0,0 +1,466 @@
-- loads snippets from directory structured almost like snipmate-collection:
-- - files all named <ft>.lua
-- - each returns table containing keys (optional) "snippets" and
-- "autosnippets", value for each a list of snippets.
--
-- cache:
-- - lazy_load_paths: {
-- {
-- add_opts = {...},
-- ft1 = {filename1, filename2},
-- ft2 = {filename1},
-- ...
-- }, {
-- add_opts = {...},
-- ft1 = {filename1},
-- ...
-- }
-- }
--
-- each call to load generates a new entry in that list. We cannot just merge
-- all files for some ft since add_opts might be different (they might be from
-- different lazy_load-calls).
local loader_util = require("luasnip.loaders.util")
local log = require("luasnip.util.log").new("lua-loader")
local session = require("luasnip.session")
local util = require("luasnip.util.util")
local autotable = require("luasnip.util.auto_table").autotable
local tree_watcher = require("luasnip.loaders.fs_watchers").tree
local path_watcher = require("luasnip.loaders.fs_watchers").path
local digraph = require("luasnip.util.directed_graph")
local refresh_notify =
require("luasnip.session.enqueueable_operations").refresh_notify
local clean_invalidated =
require("luasnip.session.enqueueable_operations").clean_invalidated
local Data = require("luasnip.loaders.data")
local M = {}
-- ASSUMPTION: this function will only be called inside the snippet-constructor,
-- to find the location of the lua-loaded file calling it.
-- It is not exported, because it will (in its current state) only ever be used
-- in one place, and it feels a bit wrong to expose put a function into `M`.
-- Instead, it is inserted into the global environment before a luasnippet-file
-- is loaded, and removed from it immediately when this is done
local function get_loaded_file_debuginfo()
-- we can skip looking at the first four stackframes, since
-- 1 is this function
-- 2 is the snippet-constructor
-- ... (here anything is going on, could be 0 stackframes, could be many)
-- n-2 (at least 3) is the loaded file
-- n-1 (at least 4) is pcall
-- n (at least 5) is _luasnip_load_file
local current_call_depth = 4
local debuginfo
repeat
current_call_depth = current_call_depth + 1
debuginfo = debug.getinfo(current_call_depth, "n")
until debuginfo.name == "_luasnip_load_file"
-- ret is stored into a local, and not returned immediately to prevent tail
-- call optimization, which seems to invalidate the stackframe-numbers
-- determined earlier.
--
-- current_call_depth-0 is _luasnip_load_file,
-- current_call_depth-1 is pcall, and
-- current_call_depth-2 is the lua-loaded file.
-- "Sl": get only source-file and current line.
local ret = debug.getinfo(current_call_depth - 2, "Sl")
return ret
end
local function search_lua_rtp(modulename)
-- essentially stolen from vim.loader.
local rtp_lua_path = package.path
for _, path in ipairs(vim.api.nvim_get_runtime_file("", true)) do
rtp_lua_path = rtp_lua_path
.. (";%s/lua/?.lua;%s/lua/?/init.lua"):format(path, path)
end
return package.searchpath(modulename, rtp_lua_path)
end
local function _luasnip_load_file(file)
-- vim.loader.enabled does not seem to be official api, so always reset
-- if the loader is available.
-- To be sure, even pcall it, in case there are conditions under which
-- it might error.
if vim.loader then
-- pcall, not sure if this can fail in some way..
-- Does not seem like it though
local ok, res = pcall(vim.loader.reset, file)
if not ok then
log.warn("Could not reset cache for file %s\n: %s", file, res)
end
end
local func, error_msg = loadfile(file)
if error_msg then
log.error("Failed to load %s\n: %s", file, error_msg)
error(string.format("Failed to load %s\n: %s", file, error_msg))
end
-- the loaded file may add snippets to these tables, they'll be
-- combined with the snippets returned regularly.
local file_added_snippets = {}
local file_added_autosnippets = {}
local dependent_files = {}
-- setup snip_env in func
local func_env
local function ls_tracked_dofile(filename)
local package_func, err_msg = loadfile(filename)
if package_func then
setfenv(package_func, func_env)
table.insert(dependent_files, filename)
else
error(("File %s could not be loaded: %s"):format(filename, err_msg))
end
return package_func()
end
func_env = vim.tbl_extend(
"force",
-- extend the current(expected!) globals with the snip_env, and the
-- two tables.
_G,
session.get_snip_env(),
{
ls_file_snippets = file_added_snippets,
ls_file_autosnippets = file_added_autosnippets,
ls_tracked_dofile = ls_tracked_dofile,
ls_tracked_dopackage = function(package_name)
local package_file = search_lua_rtp(package_name)
if not package_file then
error(
("Could not find package %s in rtp and package.path"):format(
package_name
)
)
end
return ls_tracked_dofile(package_file)
end,
}
)
-- defaults snip-env requires metatable for resolving
-- lazily-initialized keys. If we have to combine this with an eventual
-- metatable of _G, look into unifying ls.setup_snip_env and this.
setmetatable(func_env, getmetatable(session.get_snip_env()))
setfenv(func, func_env)
-- Since this function has to reach the snippet-constructor, and fenvs
-- aren't inherited by called functions, we have to set it in the global
-- environment.
_G.__luasnip_get_loaded_file_frame_debuginfo = util.ternary(
session.config.loaders_store_source,
get_loaded_file_debuginfo,
nil
)
local run_ok, file_snippets, file_autosnippets = pcall(func)
-- immediately nil it.
_G.__luasnip_get_loaded_file_frame_debuginfo = nil
if not run_ok then
log.error("Failed to execute\n: %s", file, file_snippets)
error("Failed to execute " .. file .. "\n: " .. file_snippets)
end
-- make sure these aren't nil.
file_snippets = file_snippets or {}
file_autosnippets = file_autosnippets or {}
vim.list_extend(file_snippets, file_added_snippets)
vim.list_extend(file_autosnippets, file_added_autosnippets)
return file_snippets, file_autosnippets, dependent_files
end
local function lua_package_file_filter(fname)
return fname:match("%.lua$")
end
--- Collection watches all files that belong to a collection of snippets below
--- some root, and registers new files.
local Collection = {}
local Collection_mt = {
__index = Collection,
}
function Collection.new(
root,
lazy,
include_ft,
exclude_ft,
add_opts,
lazy_watcher,
fs_event_providers
)
local ft_filter = loader_util.ft_filter(include_ft, exclude_ft)
local o = setmetatable({
root = root,
file_filter = function(path, ft)
if not path:sub(1, #root) == root then
log.warn(
"Tried to filter file `%s`, which is not inside the root `%s`.",
path,
root
)
return false
end
return lua_package_file_filter(path) and ft_filter(ft)
end,
add_opts = add_opts,
lazy = lazy,
-- store ft -> set of files that should be lazy-loaded.
lazy_files = autotable(2, { warn = false }),
-- store, for all files in this collection, their filetype.
-- No need to always recompute it, and we can use this to store which
-- files belong to the collection.
loaded_path_ft = {},
file_dependencies = digraph.new_labeled(),
-- store fs_watchers for files the snippets-files depend on.
dependency_watchers = {},
fs_event_providers = fs_event_providers,
}, Collection_mt)
-- only register files up to a depth of 2.
local ok, err_or_watcher = pcall(tree_watcher, root, 2, {
-- don't handle removals for now.
new_file = function(path)
local path_ft = loader_util.collection_file_ft(o.root, path)
-- detected new file, make sure it is allowed by our filters.
if o.file_filter(path, path_ft) then
o:add_file(path, path_ft)
end
end,
change_file = function(path)
o:reload(path)
end,
}, { lazy = lazy_watcher, fs_event_providers = fs_event_providers })
if not ok then
error(("Could not create watcher: %s"):format(err_or_watcher))
end
o.watcher = err_or_watcher
log.info("Initialized snippet-collection at `%s`", root)
return o
end
-- Add file with some filetype to collection.
function Collection:add_file(path, ft)
Data.lua_ft_paths[ft][path] = true
if self.lazy then
if not session.loaded_fts[ft] then
log.info(
"Registering lazy-load-snippets for ft `%s` from file `%s`",
ft,
path
)
-- only register to load later.
self.lazy_files[ft][path] = true
return
else
log.info(
"Filetype `%s` is already active, loading immediately.",
ft
)
end
end
self:load_file(path, ft)
end
function Collection:load_file(path, ft)
log.info("Adding snippets for filetype `%s` from file `%s`", ft, path)
self.loaded_path_ft[path] = ft
local snippets, autosnippets, dependent_files = _luasnip_load_file(path)
-- ignored if it already exists.
self.file_dependencies:set_vertex(path)
-- make sure we don't retain any old dependencies.
self.file_dependencies:clear_edges(path)
for _, file_dependency in ipairs(dependent_files) do
-- ignored if it already exists.
self.file_dependencies:set_vertex(file_dependency)
-- path depends on dependent_file => if dependent_file is changed, path
-- should be updated.
self.file_dependencies:set_edge(file_dependency, path, path)
if not self.dependency_watchers[file_dependency] then
self.dependency_watchers[file_dependency] = path_watcher(
file_dependency,
{
change = function(_)
local depending_files =
self.file_dependencies:connected_component(
file_dependency,
"Forward"
)
for _, file in ipairs(depending_files) do
-- Prevent loading one of the utility-files as a snippet-file.
-- This will not reject any snippet-file in
-- depending_files. This is because since they are in
-- depending_files, we have their dependency-information,
-- which can only be obtained by loading them, and so there
-- can't be any unloaded files in there.
if self.loaded_path_ft[file] then
self:load_file(file, self.loaded_path_ft[file])
end
end
end,
},
{ lazy = false, fs_event_providers = self.fs_event_providers }
)
end
end
loader_util.add_file_snippets(
ft,
path,
snippets,
autosnippets,
self.add_opts
)
refresh_notify(ft)
end
function Collection:do_lazy_load(ft)
for file, _ in pairs(self.lazy_files[ft]) do
if not self.loaded_path_ft[file] then
self:load_file(file, ft)
end
end
end
-- will only do something, if the file at `path` was loaded previously.
function Collection:reload(path)
local path_ft = self.loaded_path_ft[path]
if not path_ft then
-- file not yet loaded.
return
end
-- will override previously-loaded snippets from this path.
self:load_file(path, path_ft)
-- clean snippets if enough were removed.
clean_invalidated()
end
function Collection:stop()
self.watcher:stop()
for _, watcher in pairs(self.dependency_watchers) do
watcher:stop()
end
end
function M._load_lazy_loaded_ft(ft)
log.info("Loading lazy-load-snippets for filetype `%s`", ft)
for _, collection in ipairs(Data.lua_collections) do
collection:do_lazy_load(ft)
end
end
local function _load(lazy, opts)
local o = loader_util.normalize_opts(opts)
local collection_roots =
loader_util.resolve_root_paths(o.paths, "luasnippets")
local lazy_roots = loader_util.resolve_lazy_root_paths(o.lazy_paths)
log.info(
"Found roots `%s` for paths `%s`.",
vim.inspect(collection_roots),
vim.inspect(o.paths)
)
if o.paths and #o.paths ~= #collection_roots then
log.warn(
"Could not resolve all collection-roots for paths `%s`: only found `%s`",
vim.inspect(o.paths),
vim.inspect(collection_roots)
)
end
log.info(
"Determined roots `%s` for lazy_paths `%s`.",
vim.inspect(lazy_roots),
vim.inspect(o.lazy_paths)
)
if o.lazy_paths and #o.lazy_paths ~= #lazy_roots then
log.warn(
"Could not resolve all collection-roots for lazy_paths `%s`: only found `%s`",
vim.inspect(o.lazy_paths),
vim.inspect(lazy_roots)
)
end
for paths_lazy, roots in pairs({
[true] = lazy_roots,
[false] = collection_roots,
}) do
for _, collection_root in ipairs(roots) do
local ok, coll_or_err = pcall(
Collection.new,
collection_root,
lazy,
o.include,
o.exclude,
o.add_opts,
paths_lazy,
o.fs_event_providers
)
if not ok then
log.error(
"Could not create collection at %s: %s",
collection_root,
coll_or_err
)
else
table.insert(Data.lua_collections, coll_or_err)
end
end
end
end
--- Load lua-snippet-collections immediately.
--- @param opts LuaSnip.Loaders.LoadOpts?
function M.load(opts)
_load(false, opts)
end
--- Load lua-snippet-collections on demand.
--- @param opts LuaSnip.Loaders.LoadOpts?
function M.lazy_load(opts)
_load(true, opts)
-- load for current buffer on startup.
for _, ft in
ipairs(loader_util.get_load_fts(vim.api.nvim_get_current_buf()))
do
M._load_lazy_loaded_ft(ft)
end
end
function M.clean()
for _, collection in ipairs(Data.lua_collections) do
collection:stop()
end
-- bit ugly, keep in sync with defaults in data.lua.
-- Don't anticipate those changing, so fine I guess.
Data.lua_collections = {}
Data.lua_ft_paths = autotable(2)
end
return M

View File

@ -0,0 +1,512 @@
local loader_util = require("luasnip.loaders.util")
local util = require("luasnip.util.util")
local tbl_util = require("luasnip.util.table")
local Path = require("luasnip.util.path")
local autotable = require("luasnip.util.auto_table").autotable
local digraph = require("luasnip.util.directed_graph")
local tree_watcher = require("luasnip.loaders.fs_watchers").tree
local Data = require("luasnip.loaders.data")
local session = require("luasnip.session")
local snippetcache = require("luasnip.loaders.snippet_cache")
local refresh_notify =
require("luasnip.session.enqueueable_operations").refresh_notify
local clean_invalidated =
require("luasnip.session.enqueueable_operations").clean_invalidated
local log = require("luasnip.util.log").new("snipmate-loader")
--- Load data from any snippet-file.
--- @param filename string
--- @return LuaSnip.Loaders.SnippetFileData
local function load_snipmate(filename)
local buffer_ok, buffer = pcall(Path.read_file, filename)
if not buffer_ok then
log.error(("Could not read file %s: %s"):format(filename, buffer))
-- return dummy-data.
return {
snippets = {},
autosnippets = {},
misc = {},
}
end
local sp = require("luasnip.nodes.snippetProxy")
local snipmate_parse_fn = require("luasnip.util.parser").parse_snipmate
local source = require("luasnip.session.snippet_collection.source")
-- could also be separate variables, but easier to access this way.
local snippets = {
snippet = {},
autosnippet = {},
}
local extends = {}
---@type string[]
local lines = loader_util.split_lines(buffer)
local i = 1
local function _parse(snippet_type, snipmate_opts)
local line = lines[i]
-- "snippet" or "autosnippet"
local prefix, description =
line:match("^" .. snippet_type .. [[%s+(%S+)%s*(.*)]])
local body = {}
local snip_begin_line = i
i = i + 1
---@type number
local indent
while i <= #lines do
line = lines[i]
if line:find("^%s+") then
if not indent then
indent = #line:match("^%s+")
end
line = line:sub(indent + 1)
line = line:gsub("${VISUAL}", "${TM_SELECTED_TEXT}")
elseif line ~= "" then
break
end
table.insert(body, line)
i = i + 1
end
body = table.concat(body, "\n")
local snip = sp(
{
trig = prefix,
desc = description,
wordTrig = true,
priority = snipmate_opts.priority,
},
body,
{
parse_fn = snipmate_parse_fn,
}
)
if session.config.loaders_store_source then
snip._source = source.from_location(
filename,
{ line = snip_begin_line, line_end = i - 1 }
)
end
table.insert(snippets[snippet_type], snip)
end
-- options for some snippet can be specified in the lines before the
-- {auto}snippet-keyword ("priority 2000\nsnippet....").
-- They are stored in snipmate_opts, which is cleaned whenever a snippet is
-- actually created.
local snipmate_opts = {}
while i <= #lines do
local line = lines[i]
if vim.startswith(line, "snippet") then
_parse("snippet", snipmate_opts)
snipmate_opts = {}
elseif vim.startswith(line, "autosnippet") then
_parse("autosnippet", snipmate_opts)
snipmate_opts = {}
elseif vim.startswith(line, "extends") then
vim.list_extend(extends, vim.split(vim.trim(line:sub(8)), "[,%s]+"))
i = i + 1
elseif vim.startswith(line, "#") or line:find("^%s*$") then
-- comment and blank line
i = i + 1
elseif vim.startswith(line, "priority") then
snipmate_opts.priority = tonumber(line:match("priority%s+(%d+)"))
i = i + 1
else
log.error("Invalid line in %s: %s", filename, i)
error(("Invalid line in %s: %s"):format(filename, i))
end
end
return {
snippets = snippets.snippet,
autosnippets = snippets.autosnippet,
misc = extends,
}
end
-- cache snippets without filetype-association for reuse.
Data.snipmate_cache = snippetcache.new(load_snipmate)
--- Collection watches all files that belong to a collection of snippets below
--- some root, and registers new files.
local Collection = {}
local Collection_mt = {
__index = Collection,
}
local function snipmate_package_file_filter(fname)
return fname:match("%.snippets$")
end
function Collection.new(
root,
lazy,
include_ft,
exclude_ft,
add_opts,
lazy_watcher,
fs_event_providers
)
local ft_filter = loader_util.ft_filter(include_ft, exclude_ft)
local o = setmetatable({
root = root,
--- @alias LuaSnip.Loaders.Snipmate.FileCategory
--- | '"collection"' File only belongs to the collection
--- | '"load"' File should be loaded
--- Determine whether a file should be loaded, belongs to the
--- collection, or doesn't.
--- This distinction is important because we need to know about all
--- files to correctly resolve `extend <someft>`, but only want to load
--- the filetypes allowed by in/exclude.
--- @param path string
---@return LuaSnip.Loaders.Snipmate.FileCategory?
categorize_file = function(path)
if not path:sub(1, #root) == root then
log.warn(
"Tried to filter file `%s`, which is not inside the root `%s`.",
path,
root
)
return nil
end
if snipmate_package_file_filter(path) then
local path_ft = loader_util.collection_file_ft(root, path)
if ft_filter(path_ft) then
return "load"
end
return "collection"
end
return nil
end,
-- sometimes we don't want the full categorize-file-data, only which
-- filetypes should be loaded.
ft_filter = ft_filter,
add_opts = add_opts,
lazy = lazy,
-- store ft -> set of files that should be lazy-loaded.
lazy_files = autotable(2, { warn = false }),
-- store for each path the set of filetypes it has been loaded with.
loaded_path_fts = autotable(2, { warn = false }),
-- model filetype-extensions (`extends <someft>` in `ft.snippets`).
-- Better than a flat table with t[ft] = {someft=true, somotherft=true}
-- since transitive dependencies are easier to understand/query.
-- There is an edge with source src to destination dst, if snippets for
-- filetype src also contribute to filetype dst.
-- Since we respect transitive `extends`, we can get all filetypes a
-- snippet-file for some filetype A contributes to by querying the
-- connected component of A (all filetype-vertices reachable from A).
ft_extensions = digraph.new_labeled(),
-- store all files in the collection, by their filetype.
-- This information is necessary to handle `extends` even for files
-- that are not actually loaded (due to in/exclude).
collection_files_by_ft = autotable(2, { warn = false }),
-- set if creation successful.
watcher = nil,
}, Collection_mt)
-- only register files up to a depth of 2.
local ok, err_or_watcher = pcall(tree_watcher, root, 2, {
-- don't handle removals for now.
new_file = function(path)
---@as LuaSnip.Loaders.Snipmate.FileCategory
local file_category = o.categorize_file(path)
if file_category then
-- know it's at least in the collection -> can register it.
local file_ft = loader_util.collection_file_ft(o.root, path)
o:register_file(path, file_ft)
if file_category == "load" then
-- actually load if allowed by in/exclude.
o:add_file(path, file_ft)
end
end
end,
change_file = function(path)
vim.schedule_wrap(function()
o:reload(path)
end)()
end,
}, { lazy = lazy_watcher, fs_event_providers = fs_event_providers })
if not ok then
error(("Could not create watcher: %s"):format(err_or_watcher))
end
o.watcher = err_or_watcher
log.info("Initialized snippet-collection at `%s`", root)
return o
end
--- Makes the file known to the collection, but does not load its snippets.
--- This is important because `extends` may require loading a file excluded by
--- `file_filter`, ie `include` and `exclude`.
--- @param path string
--- @param ft string
function Collection:register_file(path, ft)
self.collection_files_by_ft[ft][path] = true
end
--- Register a file-filetype-association with the collection.
--- @param path string Path to a file that belongs to this collection.
--- @param add_ft string The original filetype this file should be added as.
--- Since we have to support filetype-extensions, this may
--- add the snippets of the file to several other
--- filetypes.
function Collection:add_file(path, add_ft)
-- register known file.
Data.snipmate_ft_paths[add_ft][path] = true
if self.lazy then
if not session.loaded_fts[add_ft] then
log.info(
"Registering lazy-load-snippets for ft `%s` from file `%s`",
add_ft,
path
)
-- only register to load later.
self.lazy_files[add_ft][path] = true
return
else
log.info(
"Filetype `%s` is already active, loading immediately.",
add_ft
)
end
end
-- extended filetypes will be loaded in load_file.
self:load_file(path, add_ft, "SkipIfLoaded")
end
--- @alias LuaSnip.Loaders.Snipmate.SkipLoad
--- | '"ForceLoad"' Always load, even if it was already loaded.
--- | '"SkipIfLoaded"' Skip the load if the file has been loaded already.
-- loads the fts that extend load_ft as well.
-- skip_load_mode allows this code to both prevent unnecessary loads (which
-- could be caused if some file is added to the same filetype more than once),
-- while still handling reload (where the files has to be loaded again for
-- every filetype, even if it already is loaded (since it may have different
-- snippets))
function Collection:load_file(path, ft, skip_load_mode)
if skip_load_mode == "SkipIfLoaded" and self.loaded_path_fts[path][ft] then
return
end
-- ignore load_file if ft is excluded/not included.
if not self.ft_filter(ft) then
return
end
log.info("Adding snippets for filetype `%s` from file `%s`", ft, path)
-- Set here to skip loads triggered for the same path-file-combination in
-- subsequent code, which would trigger and endless loop.
self.loaded_path_fts[path][ft] = true
-- this may already be set, but setting again here ensures that a file is
-- certainly associated with each filetype it's loaded for. (for example,
-- file-ft-combinations loaded as a dependency from another file may not be
-- set already).
Data.snipmate_ft_paths[ft][path] = true
-- snippets may already be loaded -> get them from cache.
local data = Data.snipmate_cache:fetch(path)
local snippets = data.snippets
local autosnippets = data.autosnippets
-- data.misc is user-input, clean it here.
local extended_fts = util.deduplicate(data.misc)
-- ignored if it already exists.
self.ft_extensions:set_vertex(ft)
-- make sure we don't retain any old dependencies.
self.ft_extensions:clear_edges(path)
for _, extended_ft in pairs(extended_fts) do
-- ignored if it already exists.
self.ft_extensions:set_vertex(extended_ft)
-- snippets for extended_ft should also be loaded if ft is loaded
-- label edge with path, so all edges from this file can be updated on
-- reload.
self.ft_extensions:set_edge(extended_ft, ft, path)
end
loader_util.add_file_snippets(
ft,
path,
snippets,
autosnippets,
self.add_opts
)
-- get all filetypes this one extends (directly or transitively), and load
-- their files for ft.
local load_fts = self.ft_extensions:connected_component(ft, "Backward")
for _, extended_ft in ipairs(load_fts) do
for file, _ in pairs(self.collection_files_by_ft[extended_ft]) do
-- check which filetypes a file with filetype extended_ft has to be
-- loaded for currently!! (ie. which filetypes directly or
-- transitively extend extended_ft.
-- The reason we can't just check that statically once, or just load
-- a file for the filetypes its filetype is initially extended by is
-- that the extends-graph may change on each `load_file`, and we
-- want to respect these changes.
for _, file_ft in
ipairs(
self.ft_extensions:connected_component(
extended_ft,
"Forward"
)
)
do
-- skips load if the file is already loaded for the given filetype.
-- One bad side-effect of this current implementation is that
-- the edges in the graph will be reset/set multiple times,
-- until they are retained in the last load_file-call to the
-- last filetype.
self:load_file(file, file_ft, "SkipIfLoaded")
end
end
end
refresh_notify(ft)
end
function Collection:do_lazy_load(lazy_ft)
for file, _ in pairs(self.lazy_files[lazy_ft]) do
for _, ft in
ipairs(self.ft_extensions:connected_component(lazy_ft, "Forward"))
do
-- skips load if the file is already loaded for the given filetype.
self:load_file(file, ft, "SkipIfLoaded")
end
end
end
-- will only do something, if the file at `path` is actually in the collection.
function Collection:reload(path)
local loaded_fts = tbl_util.set_to_list(self.loaded_path_fts[path])
for _, loaded_ft in ipairs(loaded_fts) do
-- will override previously-loaded snippets from this path.
self:load_file(path, loaded_ft, "ForceLoad")
end
-- clean snippets if enough were removed.
clean_invalidated()
end
function Collection:stop()
self.watcher:stop()
end
local M = {}
function M._load_lazy_loaded_ft(ft)
log.info("Loading lazy-load-snippets for filetype `%s`", ft)
for _, collection in ipairs(Data.snipmate_collections) do
collection:do_lazy_load(ft)
end
end
--- Generalized loading of collections.
--- @param lazy boolean Whether the collection should be loaded lazily.
--- @param opts LuaSnip.Loaders.LoadOpts?
local function _load(lazy, opts)
local o = loader_util.normalize_opts(opts)
local collection_roots = loader_util.resolve_root_paths(o.paths, "snippets")
local lazy_roots = loader_util.resolve_lazy_root_paths(o.lazy_paths)
log.info(
"Found roots `%s` for paths `%s`.",
vim.inspect(collection_roots),
vim.inspect(o.paths)
)
if o.paths and #o.paths ~= #collection_roots then
log.warn(
"Could not resolve all collection-roots for paths `%s`: only found `%s`",
vim.inspect(o.paths),
vim.inspect(collection_roots)
)
end
log.info(
"Determined roots `%s` for lazy_paths `%s`.",
vim.inspect(lazy_roots),
vim.inspect(o.lazy_paths)
)
if o.lazy_paths and #o.lazy_paths ~= #lazy_roots then
log.warn(
"Could not resolve all collection-roots for lazy_paths `%s`: only found `%s`",
vim.inspect(o.lazy_paths),
vim.inspect(lazy_roots)
)
end
for paths_lazy, roots in pairs({
[true] = lazy_roots,
[false] = collection_roots,
}) do
for _, collection_root in ipairs(roots) do
local ok, coll_or_err = pcall(
Collection.new,
collection_root,
lazy,
o.include,
o.exclude,
o.add_opts,
paths_lazy,
o.fs_event_providers
)
if not ok then
log.error(
"Could not create collection at %s: %s",
collection_root,
coll_or_err
)
else
table.insert(Data.snipmate_collections, coll_or_err)
end
end
end
end
--- Load snipmate-snippet-collections immediately.
--- @param opts LuaSnip.Loaders.LoadOpts?
function M.load(opts)
_load(false, opts)
end
--- Load snipmate-snippet-collections on demand.
--- @param opts LuaSnip.Loaders.LoadOpts?
function M.lazy_load(opts)
_load(true, opts)
-- load for current buffer on startup.
for _, ft in
ipairs(loader_util.get_load_fts(vim.api.nvim_get_current_buf()))
do
M._load_lazy_loaded_ft(ft)
end
end
function M.clean()
for _, collection in ipairs(Data.snipmate_collections) do
collection:stop()
end
Data.snipmate_ft_paths = autotable(2)
-- don't reset cache, snippets are correctly updated on file-change anyway,
-- and there is no persistent data passed on.
end
return M

View File

@ -0,0 +1,645 @@
local util = require("luasnip.util.util")
local loader_util = require("luasnip.loaders.util")
local Path = require("luasnip.util.path")
local log = require("luasnip.util.log").new("vscode-loader")
local autotable = require("luasnip.util.auto_table").autotable
local path_watcher = require("luasnip.loaders.fs_watchers").path
local Data = require("luasnip.loaders.data")
local session = require("luasnip.session")
local refresh_notify =
require("luasnip.session.enqueueable_operations").refresh_notify
local clean_invalidated =
require("luasnip.session.enqueueable_operations").clean_invalidated
-- create snippetProxy which does not trim lines and dedent text.
-- It's fair to use passed test as-is, if it's from json.
local parse = require("luasnip.util.parser").parse_snippet
local sp = require("luasnip.util.extend_decorator").apply(
require("luasnip.nodes.snippetProxy"),
{},
{
parse_fn = function(ctx, body)
return parse(ctx, body, { trim_empty = false, dedent = false })
end,
}
)
local json_decoders = {
json = util.json_decode,
jsonc = require("luasnip.util.jsonc").decode,
["code-snippets"] = require("luasnip.util.jsonc").decode,
}
local function read_json(fname)
local data_ok, data = pcall(Path.read_file, fname)
if not data_ok then
log.error("Could not read file %s", fname)
return nil
end
local fname_extension = Path.extension(fname)
if json_decoders[fname_extension] == nil then
log.error(
"`%s` was expected to have file-extension either `json`, `jsonc` or `code-snippets`, but doesn't.",
fname
)
return nil
end
local fname_decoder = json_decoders[fname_extension]
local status, result = pcall(fname_decoder, data)
if status then
return result
else
log.error("Could not parse file %s: %s", fname, result)
return nil
end
end
--- Load snippets from vscode-snippet-file.
--- @param file string Path to file
---@return LuaSnip.Loaders.SnippetFileData
local function get_file_snippets(file)
local source = require("luasnip.session.snippet_collection.source")
local multisnippet = require("luasnip.nodes.multiSnippet")
-- since most snippets we load don't have a scope-field, we just insert
-- them here by default.
local snippets = {}
local snippet_set_data = read_json(file)
if snippet_set_data == nil then
log.error("Reading json from file `%s` failed, skipping it.", file)
return {
snippets = {},
autosnippets = {},
misc = {},
}
end
for name, parts in pairs(snippet_set_data) do
local body = type(parts.body) == "string" and parts.body
or table.concat(parts.body, "\n")
local ls_conf = parts.luasnip or {}
-- we may generate multiple interfaces to the same snippet
-- (different filetype, different triggers)
-- context common to all snippets generated here.
local common_context = {
name = name,
desc = parts.description or name,
wordTrig = ls_conf.wordTrig,
priority = ls_conf.priority,
snippetType = ls_conf.autotrigger and "autosnippet" or "snippet",
}
-- Sometimes it's a list of prefixes instead of a single one
local prefixes = type(parts.prefix) == "table" and parts.prefix
or { parts.prefix }
-- vscode documents `,`, but `.` also works.
-- an entry `false` in this list will cause a `ft=nil` for the snippet.
local filetypes = parts.scope and vim.split(parts.scope, "[.,]")
or { false }
local contexts = {}
for _, prefix in ipairs(prefixes) do
for _, filetype in ipairs(filetypes) do
table.insert(
contexts,
{ filetype = filetype or nil, trig = prefix }
)
end
end
local snip
if #contexts > 1 then
-- only construct multisnippet if it is actually necessary.
contexts.common = common_context
snip = multisnippet._raw_ms(contexts, sp(nil, body), {})
elseif #contexts == 1 then
-- have to add options from common context to the trig/filetype-context.
snip = sp(vim.tbl_extend("keep", contexts[1], common_context), body)
end
if snip then
if session.config.loaders_store_source then
-- only know file, not line or line_end.
snip._source = source.from_location(file)
end
table.insert(snippets, snip)
end
end
return {
snippets = snippets,
autosnippets = {},
misc = {},
}
end
-- has to be set in separate module to allow different module-path-separators
-- in `require`.
Data.vscode_cache =
require("luasnip.loaders.snippet_cache").new(get_file_snippets)
--- Parse package.json(c), determine all files that contribute snippets, and
--- which filetype is associated with them.
--- @param manifest string
--- @return table<string, table<string, true|nil>>
local function get_snippet_files(manifest)
-- if root doesn't contain a package.json, or it contributes no snippets,
-- return no snippets.
if not Path.exists(manifest) then
log.warn("Manifest %s does not exist", manifest)
return {}
end
local package_data = read_json(manifest)
if not package_data then
-- since it is a `.json/jsonc`, the json not being correct should be an error.
log.error("Could not read json from `%s`", manifest)
return {}
end
if
not package_data.contributes or not package_data.contributes.snippets
then
log.warn("Manifest %s does not contribute any snippets.", manifest)
return {}
end
-- stores ft -> files -> true|nil, allow iterating files and their
-- filetypes while preventing duplicates.
local ft_file_set = autotable(2, { warn = false })
-- parent-directory of package.json(c), all files in the package.json(c)
-- are relative to it.
local package_parent = Path.parent(manifest)
for _, snippet_entry in pairs(package_data.contributes.snippets) do
local absolute_path = Path.join(package_parent, snippet_entry.path)
local normalized_snippet_file = Path.normalize(absolute_path)
if not normalized_snippet_file then
-- path does not exist (yet), try and guess the correct path anyway.
normalized_snippet_file = Path.normalize_nonexisting(absolute_path)
log.warn(
"Could not find file %s advertised in %s, guessing %s as the absolute and normalized path.",
absolute_path,
manifest,
normalized_snippet_file
)
end
local langs = snippet_entry.language
if type(langs) ~= "table" then
langs = { langs }
end
for _, ft in ipairs(langs) do
ft_file_set[ft][normalized_snippet_file] = true
end
end
return ft_file_set
end
-- Responsible for watching a single json-snippet-file.
local SnippetfileWatcher = {}
local SnippetfileWatcher_mt = { __index = SnippetfileWatcher }
function SnippetfileWatcher.new(
path,
initial_ft,
fs_event_providers,
lazy,
load_cb
)
local o = setmetatable({
path = path,
load_cb = load_cb,
-- track which filetypes this file has been loaded for, so we can
-- reload for all of them.
loaded_fts = { [initial_ft] = true },
}, SnippetfileWatcher_mt)
local load_all_fts = function()
for ft, _ in pairs(o.loaded_fts) do
load_cb(path, ft)
refresh_notify(ft)
end
end
local ok, err_or_watcher = pcall(path_watcher, path, {
add = load_all_fts,
change = function()
load_all_fts()
-- clean snippets if enough were removed.
clean_invalidated()
end,
}, { lazy = lazy, fs_event_providers = fs_event_providers })
if not ok then
-- has to be handled by caller, we can't really proceed if the creation
-- failed.
error(
("Could not create path_watcher for path %s: %s"):format(
path,
err_or_watcher
)
)
end
o.watcher = err_or_watcher
return o
end
-- called by collection.
function SnippetfileWatcher:add_ft(ft)
if self.loaded_fts[ft] then
-- already loaded.
return
end
self.loaded_fts[ft] = true
self.load_cb(self.path, ft)
end
function SnippetfileWatcher:stop()
self.watcher:stop()
end
--- Collection watches all files that belong to a collection of snippets below
--- some root, and registers new files.
local Collection = {}
local Collection_mt = {
__index = Collection,
}
function Collection.new(
manifest_path,
lazy,
include_ft,
exclude_ft,
add_opts,
lazy_watcher,
fs_event_providers
)
local ft_filter = loader_util.ft_filter(include_ft, exclude_ft)
local o = setmetatable({
lazy = lazy,
-- store ft -> set of files that should be lazy-loaded.
lazy_files = autotable(2, { warn = false }),
fs_event_providers = fs_event_providers,
-- store path-watchers (so we don't register more than one for one
-- path), and so we can disable them.
path_watchers = {},
-- for really loading a file.
-- this is not done in Collection:load itself, since it may have to be
-- performed as a callback on file-creation.
load_callback = function(path, ft)
local data = Data.vscode_cache:fetch(path)
-- autosnippets are included in snippets for this loader.
local snippets = data.snippets
loader_util.add_file_snippets(ft, path, snippets, {}, add_opts)
end,
-- initialized in a bit, we have to store+reset a watcher for the manifest-file.
manifest_watcher = nil,
}, Collection_mt)
-- callback for updating the file-filetype-associations from the manifest.
local update_manifest = function()
local manifest_ft_paths = get_snippet_files(manifest_path)
for ft, path_set in pairs(manifest_ft_paths) do
if ft_filter(ft) then
for path, _ in pairs(path_set) do
o:add_file(path, ft)
end
end
end
end
local ok, watcher_or_err = pcall(path_watcher, manifest_path, {
-- don't handle removals for now.
add = update_manifest,
change = update_manifest,
}, { lazy = lazy_watcher, fs_event_providers = fs_event_providers })
if not ok then
error(("Could not create watcher: %s"):format(watcher_or_err))
end
o.manifest_watcher = watcher_or_err
log.info("Initialized snippet-collection with manifest %s", manifest_path)
return o
end
-- Add file with some filetype to collection, load according to lazy_load.
function Collection:add_file(path, ft)
Data.vscode_ft_paths[ft][path] = true
if self.lazy then
if not session.loaded_fts[ft] then
log.info(
"Registering lazy-load-snippets for ft `%s` from file `%s`",
ft,
path
)
-- only register to load later.
self.lazy_files[ft][path] = true
return
else
log.info(
"Filetype `%s` is already active, loading immediately.",
ft
)
end
end
self:load_file(path, ft)
end
function Collection:load_file(path, ft)
log.info("Registering file %s with filetype %s for loading.", path, ft)
if not self.path_watchers[path] then
-- always register these lazily, that way an upate to the package.json
-- without the snippet-file existing will work!
-- Also make sure we use the same fs_event_providers.
local ok, watcher_or_err = pcall(
SnippetfileWatcher.new,
path,
ft,
self.fs_event_providers,
true,
self.load_callback
)
if not ok then
log.error(
"Could not create SnippetFileWatcher for path %s: %s",
path,
watcher_or_err
)
return
end
self.path_watchers[path] = watcher_or_err
else
-- make new filetype known to existing watcher.
self.path_watchers[path]:add_ft(ft)
end
end
-- stop all watchers associated with this collection, to make sure no snippets
-- are added from this collection again.
function Collection:stop()
self.manifest_watcher:stop()
for _, watcher in pairs(self.path_watchers) do
watcher:stop()
end
end
function Collection:do_lazy_load(ft)
for file, _ in pairs(self.lazy_files[ft]) do
self:load_file(file, ft)
end
end
local M = {}
local function get_rtp_paths()
return vim.list_extend(
-- would be very surprised if this yields duplicates :D
vim.api.nvim_get_runtime_file("package.json", true),
vim.api.nvim_get_runtime_file("package.jsonc", true)
)
end
--- Generate list of manifest-paths from list of directory-paths.
--- If nil, search rtp.
--- If a given directory, or the mani
---
--- @param paths string|table? List of existing directories. If nil, search runtimepath.
---@return string[] manifest_paths
local function get_manifests(paths)
local manifest_paths = {}
-- list of paths to crawl for loading (could be a table or a comma-separated-list)
if paths then
-- Get path to package.json/package.jsonc, or continue if it does not exist.
for _, dir in ipairs(paths) do
local tentative_manifest_path =
Path.expand_keep_symlink(Path.join(dir, "package.json"))
if Path.exists(tentative_manifest_path) then
table.insert(manifest_paths, tentative_manifest_path)
else
tentative_manifest_path =
Path.expand_keep_symlink(Path.join(dir, "package.jsonc"))
if Path.exists(tentative_manifest_path) then
table.insert(manifest_paths, tentative_manifest_path)
else
log.warn(
"Could not find package.json(c) in path %s (expanded to %s).",
dir,
Path.expand(dir)
)
end
end
end
else
manifest_paths = get_rtp_paths()
end
return manifest_paths
end
--- Generate list of paths to manifests that may not yet exist, from list of
--- directories (which also may not yet exist).
--- One peculiarity: This will generate two paths for each directory, since we
--- don't know if the package.json or the package.jsonc will be created.
--- This may cause a bit of overhead (not much due to snippet-cache) if both
--- are created and contribute the same snippets, but that's unlikely and/or
--- user error :P
--- @param paths string[]
---@return string[]
local function get_lazy_manifests(paths)
local lazy_manifest_paths = {}
if paths then
-- list of directories, convert to list of existing manifest-files.
if type(paths) == "string" then
paths = vim.split(paths, ",")
end
for _, dir in ipairs(paths) do
local absolute_dir = Path.expand_maybe_nonexisting(dir)
table.insert(
lazy_manifest_paths,
Path.join(absolute_dir, "package.json")
)
table.insert(
lazy_manifest_paths,
Path.join(absolute_dir, "package.jsonc")
)
end
end
return lazy_manifest_paths
end
local function _load(lazy, opts)
local o = loader_util.normalize_opts(opts)
local manifests = get_manifests(o.paths)
local lazy_manifests = get_lazy_manifests(o.lazy_paths)
log.info(
"Found manifests `%s` for paths `%s`.",
vim.inspect(manifests),
vim.inspect(o.paths)
)
if o.paths and #o.paths ~= #manifests then
log.warn(
"Could not resolve all manifests for paths `%s`: only found `%s`",
vim.inspect(o.paths),
vim.inspect(manifests)
)
end
log.info(
"Determined roots `%s` for lazy_paths `%s`.",
vim.inspect(lazy_manifests),
vim.inspect(o.lazy_paths)
)
-- two lazy manifests from each lazy directory.
if o.lazy_paths and #o.lazy_paths ~= 2 * #lazy_manifests then
log.warn(
"Could not resolve all manifests for lazy_paths `%s`: only found `%s`",
vim.inspect(o.lazy_paths),
vim.inspect(lazy_manifests)
)
end
for is_lazy, manifest_paths in pairs({
[true] = lazy_manifests,
[false] = manifests,
}) do
for _, manifest_path in ipairs(manifest_paths) do
local ok, coll_or_err = pcall(
Collection.new,
manifest_path,
lazy,
o.include,
o.exclude,
o.add_opts,
is_lazy,
o.fs_event_providers
)
if not ok then
log.error(
"Could not create collection for manifest %s: %s",
manifest_path,
coll_or_err
)
else
table.insert(Data.vscode_package_collections, coll_or_err)
end
end
end
end
function M._load_lazy_loaded_ft(ft)
log.info("Loading lazy-load-snippets for filetype `%s`", ft)
for _, collection in ipairs(Data.vscode_package_collections) do
collection:do_lazy_load(ft)
end
-- no need to lazy_load standalone-snippets.
end
function M.load(opts)
_load(false, opts)
end
function M.lazy_load(opts)
_load(true, opts)
-- load for current buffer on startup.
for _, ft in
ipairs(loader_util.get_load_fts(vim.api.nvim_get_current_buf()))
do
M._load_lazy_loaded_ft(ft)
end
end
function M.load_standalone(opts)
opts = opts or {}
local lazy = vim.F.if_nil(opts.lazy, false)
local add_opts = loader_util.make_add_opts(opts)
local fs_event_providers =
vim.F.if_nil(opts.fs_event_providers, { autocmd = true, libuv = false })
local path
if not lazy then
path = Path.expand(opts.path)
if not path then
log.error(
"Expanding path %s does not produce an existing path.",
opts.path
)
return
end
else
path = Path.expand_maybe_nonexisting(opts.path)
end
Data.vscode_ft_paths["all"][path] = true
local ok, watcher_or_err = pcall(
SnippetfileWatcher.new,
path,
"all",
fs_event_providers,
lazy,
function()
local data = Data.vscode_cache:fetch(path)
-- autosnippets are included in snippets for this loader.
local snippets = data.snippets
loader_util.add_file_snippets("all", path, snippets, {}, add_opts)
end
)
if not ok then
log.error(
"Could not create SnippetFileWatcher for path %s: %s",
path,
watcher_or_err
)
return
end
table.insert(Data.vscode_standalone_watchers, watcher_or_err)
end
function M.clean()
for _, collection in ipairs(Data.vscode_package_collections) do
collection:stop()
end
Data.vscode_package_collections = {}
for _, standalone_watcher in ipairs(Data.vscode_standalone_watchers) do
standalone_watcher:stop()
end
Data.vscode_standalone_watchers = {}
Data.vscode_ft_paths = autotable(2)
-- don't reset cache, there's no reason to discard the already-loaded
-- snippets as long as they're unchanged.
end
return M

View File

@ -0,0 +1,723 @@
local Path = require("luasnip.util.path")
local uv = vim.uv or vim.loop
local util = require("luasnip.util.util")
local log_tree = require("luasnip.util.log").new("tree-watcher")
local log_path = require("luasnip.util.log").new("path-watcher")
local log = require("luasnip.util.log").new("fs-watchers")
local M = {}
-- used by both watchers.
local callback_mt = {
__index = function()
return util.nop
end,
}
--- @alias LuaSnip.FSWatcher.FSEventProviders
--- | '"autocmd"' Hook into BufWritePost to receive notifications on file-changes.
--- | '"libuv"' Register uv.fs_event to receive notifications on file-changes.
--- @alias LuaSnip.FSWatcher.Callback fun(full_path: string)
--- @class LuaSnip.FSWatcher.TreeCallbacks
--- @field new_file LuaSnip.FSWatcher.Callback?
--- @field new_dir LuaSnip.FSWatcher.Callback?
--- @field remove_file LuaSnip.FSWatcher.Callback?
--- @field remove_dir LuaSnip.FSWatcher.Callback?
--- @field remove_root LuaSnip.FSWatcher.Callback?
--- @field change_file LuaSnip.FSWatcher.Callback?
--- @field change_dir LuaSnip.FSWatcher.Callback?
--- The callbacks are called with the full path to the file/directory that is
--- affected.
--- Callbacks that are not set will be replaced by a nop.
--- @class LuaSnip.FSWatcher.PathCallbacks
--- @field add LuaSnip.FSWatcher.Callback?
--- @field remove LuaSnip.FSWatcher.Callback?
--- @field change LuaSnip.FSWatcher.Callback?
--- The callbacks are called with the full path to the file that path-watcher
--- is registered on.
--- Callbacks that are not set will be replaced by a nop.
--- @class LuaSnip.FSWatcher.Options
--- @field lazy boolean?
--- If set, the watcher will be initialized even if the root/watched path does
--- not yet exist, and start notifications once it is created.
--- @field fs_event_providers table<LuaSnip.FSWatcher.FSEventProviders, boolean>?
--- Which providers to use for receiving file-changes.
local function get_opts(opts)
opts = opts or {}
local lazy = vim.F.if_nil(opts.lazy, false)
local fs_event_providers =
vim.F.if_nil(opts.fs_event_providers, { autocmd = true, libuv = false })
return lazy, fs_event_providers
end
-- plain list, don't use map-style table since we'll only need direct access to
-- a watcher when it is stopped, which seldomly happens (at least, compared to
-- how often it is iterated in the autocmd-callback).
M.autocmd_watchers = {}
vim.api.nvim_create_augroup("_luasnip_fs_watcher", {})
vim.api.nvim_create_autocmd({ "BufWritePost" }, {
callback = function(args)
log.debug("Received BufWritePost for file %s.", args.file)
local realpath = Path.normalize(args.file)
if not realpath then
-- if nil, the path does not exist for some reason.
log.info(
"Registered BufWritePost with <afile> %s, but realpath does not exist. Aborting fs-watcher-notification.",
args.file
)
return
end
log.debug(
"Received update for file %s, using realpath %s.",
args.file,
realpath
)
-- remove stopped watchers.
-- Does not really matter whether we do this before or after the
-- callbacks, since stopped watchers already take care to not do
-- callbacks.
-- Doing this during the callback-invocations, however, would incur
-- some more complexity since ipairs does not support removal of
-- elements during the iteration.
M.autocmd_watchers = vim.tbl_filter(function(watcher)
-- this won't catch unstarted watchers, since they can't be in this
-- list in the first place.
return not watcher.stopped
end, M.autocmd_watchers)
for _, watcher in ipairs(M.autocmd_watchers) do
watcher:BufWritePost_callback(realpath)
end
end,
group = "_luasnip_fs_watcher",
})
-- similar autocmd_watchers, only this list contains watchers that should be
-- notified on a manual update (which right now is every watcher).
M.active_watchers = {}
function M.write_notify(realpath)
M.active_watchers = vim.tbl_filter(function(watcher)
-- this won't catch unstarted watchers, since they can't be in this
-- list in the first place.
return not watcher.stopped
end, M.active_watchers)
for _, watcher in ipairs(M.active_watchers) do
watcher:BufWritePost_callback(realpath)
end
end
--- @class LuaSnip.FSWatcher.Tree
--- @field root string
--- @field fs_event userdata
--- @field files table<string, boolean>
--- @field dir_watchers table<string, LuaSnip.FSWatcher.Tree>
--- @field removed boolean
--- @field stopped boolean
--- @field callbacks LuaSnip.FSWatcher.TreeCallbacks
--- @field depth number How deep the root should be monitored.
--- @field fs_event_providers table<LuaSnip.FSWatcher.FSEventProviders, boolean>
--- @field root_realpath string? Set as soon as the watcher is started.
local TreeWatcher = {}
local TreeWatcher_mt = {
__index = TreeWatcher,
}
function TreeWatcher:stop()
for _, child_watcher in ipairs(self.dir_watchers) do
child_watcher:stop()
end
self:stop_self()
end
function TreeWatcher:stop_self()
-- don't check which fs_event_providers were actually started, for both of
-- these it should not matter if they weren't.
self.stopped = true
self.send_notifications = false
self.fs_event:stop()
-- will be removed from active_watchers/autocmd_watchers after the next event, but already won't receive it.
end
function TreeWatcher:fs_event_callback(err, relpath, events)
if not self.send_notifications then
-- abort if we should not send notifications anymore.
return
end
vim.schedule_wrap(function()
log_tree.debug(
"raw: self.root: %s; err: %s; relpath: %s; change: %s; rename: %s",
self.root,
err,
relpath,
events.change,
events.rename
)
local full_path = Path.join(self.root, relpath)
local path_stat = uv.fs_stat(full_path)
-- try to figure out what happened in the directory.
if events.rename then
if not uv.fs_stat(self.root) then
self:remove_root()
return
end
if not path_stat then
self:remove_child(relpath, full_path)
return
end
local f_type
-- if there is a link to a directory, we are notified on changes!!
if path_stat.type == "link" then
f_type = uv.fs_stat(uv.fs_realpath(full_path))
else
f_type = path_stat.type
end
if f_type == "file" then
if self.files[relpath] then
-- rename and file exists => a new file was moved into its
-- place => handle as changed file.
self:change_file(relpath, full_path)
else
self:new_file(relpath, full_path)
end
return
elseif f_type == "directory" then
if self.dir_watchers[relpath] then
-- rename and directory exists => directory is overwritten
-- => stop recursively, clear, and start a new watcher.
self.dir_watchers[relpath]:stop()
self.dir_watchers[relpath] = nil
end
self:new_dir(relpath, full_path)
return
end
elseif events.change then
self:change_child(relpath, full_path)
end
end)()
end
-- May not recognize child correctly if there are symlinks on the path from the
-- child to the directory-root.
-- Should be fine, especially since, I think, fs_event can recognize those
-- correctly, which means that this is an issue only very seldomly.
function TreeWatcher:BufWritePost_callback(realpath)
if not self.send_notifications then
return
end
if realpath:sub(1, #self.realpath_root) ~= self.realpath_root then
-- not inside this root.
return
end
-- `#self.realpath_root+2`: remove root and path-separator.
local root_relative_components =
Path.components(realpath:sub(#self.realpath_root + 2))
local rel = root_relative_components[1]
if #root_relative_components == 1 then
-- wrote file.
-- either new, or changed.
if self.files[rel] then
-- use regular root for notifications!
self:change_file(rel, Path.join(self.root, rel))
else
self:new_file(rel, Path.join(self.root, rel))
end
else
if self.dir_watchers[rel] then
if #root_relative_components == 2 then
-- only notify if the changed file is immediately in the
-- directory we're watching!
-- I think this is the behaviour of fs_event, and logically
-- makes sense.
self:change_dir(rel, Path.join(self.root, rel))
end
else
-- does nothing if the directory already exists.
self:new_dir(rel, Path.join(self.root, rel))
end
end
end
function TreeWatcher:start()
if self.depth == 0 then
-- don't watch children for 0-depth.
return
end
if self.stopped then
-- stopping overrides and prevents starting.
return
end
self.send_notifications = true
if self.fs_event_providers.libuv then
-- does not work on nfs-drive, at least if it's edited from another
-- machine.
local success, err = self.fs_event:start(
self.root,
{},
function(err, relpath, events)
self:fs_event_callback(err, relpath, events)
end
)
if not success then
log_tree.error(
"Could not start libuv-monitor for path %s due to error %s",
self.path,
err
)
else
log_tree.info(
"Monitoring root-directory %s with libuv-monitor.",
self.root
)
end
end
-- needed by BufWritePost-callback.
self.realpath_root = Path.normalize(self.root)
if self.fs_event_providers.autocmd then
if self.realpath_root then
-- receive notifications on BufWritePost.
table.insert(M.autocmd_watchers, self)
log_tree.info(
"Monitoring root-directory %s with autocmd-monitor.",
self.root
)
else
log_tree.error(
"Could not resolve realpath for root %s, not enabling autocmd-monitor",
self.root
)
end
end
if self.realpath_root then
table.insert(M.active_watchers, self)
end
-- do initial scan after starting the watcher.
-- Scanning first, and then starting the watcher leaves a period of time
-- where a new file may be created (after scanning, before watching), where
-- we wont know about it.
-- If I understand the uv-eventloop correctly, this function, `new`, will
-- be executed completely before a callback is called, so self.files and
-- self.dir_watchers should be populated correctly when a callback is
-- received, even if it was received before all directories/files were
-- added.
-- This difference can be observed, at least on my machine, by watching a
-- directory A, and then creating a nested directory B, and children for it
-- in one command, ie. `mkdir -p A/B/{1,2,3,4,5,6,7,8,9}`.
-- If the callback is registered after the scan, the latter directories
-- (ie. 4-9) did not show up, whereas everything did work correctly if the
-- watcher was activated before the scan.
-- (almost everything, one directory was included in the initial scan and
-- the watch-event, but that seems okay for our purposes)
local files, dirs = Path.scandir(self.root)
for _, file in ipairs(files) do
local relpath = file:sub(#self.root + 2)
self:new_file(relpath, file)
end
for _, dir in ipairs(dirs) do
local relpath = dir:sub(#self.root + 2)
self:new_dir(relpath, dir)
end
end
-- these functions maintain our logical view of the directory, and call
-- callbacks when we detect a change.
function TreeWatcher:new_file(rel, full)
if self.files[rel] then
-- already added
return
end
log_tree.debug("new file %s %s", rel, full)
self.files[rel] = true
self.callbacks.new_file(full)
end
function TreeWatcher:new_dir(rel, full)
if self.dir_watchers[rel] then
-- already added
return
end
log_tree.debug("new dir %s %s", rel, full)
-- first do callback for this directory, then look into (and potentially do
-- callbacks for) children.
self.callbacks.new_dir(full)
-- directory exists => don't need to set lazy.
-- inherit fs_event_providers.
self.dir_watchers[rel] = M.tree(
full,
self.depth - 1,
self.callbacks,
{ lazy = false, fs_event_providers = self.fs_event_providers }
)
end
function TreeWatcher:change_file(rel, full)
log_tree.debug("changed file %s %s", rel, full)
self.callbacks.change_file(full)
end
function TreeWatcher:change_dir(rel, full)
log_tree.debug("changed dir %s %s", rel, full)
self.callbacks.change_dir(full)
end
function TreeWatcher:change_child(rel, full)
if self.dir_watchers[rel] then
self:change_dir(rel, full)
elseif self.files[rel] then
self:change_file(rel, full)
end
end
function TreeWatcher:remove_child(rel, full)
if self.dir_watchers[rel] then
log_tree.debug("removing dir %s %s", rel, full)
-- should have been stopped by the watcher for the child, or it was not
-- even started due to depth.
self.dir_watchers[rel]:remove_root()
self.dir_watchers[rel] = nil
self.callbacks.remove_dir(full)
elseif self.files[rel] then
log_tree.debug("removing file %s %s", rel, full)
self.files[rel] = nil
self.callbacks.remove_file(full)
end
end
function TreeWatcher:remove_root()
if self.removed then
-- already removed
return
end
log_tree.debug("removing root %s", self.root)
self.removed = true
-- stop own, children should have handled themselves, if they are watched
-- (and we don't need to do anything for unwatched children).
self:stop_self()
-- removing entries (set them to nil) is apparently fine when iterating via
-- pairs.
for relpath, _ in pairs(self.files) do
local child_full = Path.join(self.root, relpath)
self:remove_child(relpath, child_full)
end
for relpath, _ in pairs(self.dir_watchers) do
local child_full = Path.join(self.root, relpath)
self:remove_child(relpath, child_full)
end
self.callbacks.remove_root(self.root)
end
--- Set up new watcher for a tree of files and directories.
--- @param root string Absolute path to the root.
--- @param depth number The depth up to which to monitor. 1 means that the
--- immediate children will be monitored, 2 includes their
--- children, and so on.
--- @param callbacks LuaSnip.FSWatcher.TreeCallbacks The callbacks to use for this watcher.
--- @param opts LuaSnip.FSWatcher.Options Options, described in their class.
--- @return LuaSnip.FSWatcher.Tree
function M.tree(root, depth, callbacks, opts)
local lazy, fs_event_providers = get_opts(opts)
-- do nothing on missing callback.
callbacks = setmetatable(callbacks or {}, callback_mt)
local o = setmetatable({
root = root,
fs_event = uv.new_fs_event(),
files = {},
dir_watchers = {},
-- removed: have not yet triggered the removed-callback.
removed = false,
-- track whether the watcher was stopped at some point, and if it as,
-- don't allow it to start again.
stopped = false,
-- whether notifications should be sent.
-- Modified by start/stop, wait for start to send any => start out as
-- false.
send_notifications = false,
callbacks = callbacks,
depth = depth,
fs_event_providers = fs_event_providers,
}, TreeWatcher_mt)
-- if the path does not yet exist, set watcher up s.t. it will start
-- watching when the directory is created.
if not uv.fs_stat(root) and lazy then
-- root does not yet exist, need to create a watcher that notifies us
-- of its creation.
local parent_path = Path.parent(root)
if not parent_path then
error(("Could not find parent-path for %s"):format(root))
end
log_tree.info(
"Path %s does not exist yet, watching %s for creation.",
root,
parent_path
)
local parent_watcher
parent_watcher = M.tree(parent_path, 1, {
new_dir = function(full)
if full == root then
o:start()
-- directory was created, stop watching.
parent_watcher:stop_self()
end
end,
-- use same providers.
}, { lazy = true, fs_event_providers = fs_event_providers })
else
o:start()
end
return o
end
--- @class LuaSnip.FSWatcher.Path
--- @field private path string
--- @field private fs_event userdata
--- @field private removed boolean
--- @field private stopped boolean
--- @field private send_notifications boolean
--- @field private callbacks LuaSnip.FSWatcher.TreeCallbacks
--- @field private fs_event_providers table<LuaSnip.FSWatcher.FSEventProviders, boolean>
--- @field private realpath string? Set as soon as the watcher is started.
local PathWatcher = {}
local PathWatcher_mt = {
__index = PathWatcher,
}
function PathWatcher:change(full)
log_path.info("detected change at path %s", full)
if self.removed then
-- this is certainly unexpected.
log_path.warn(
"PathWatcher at %s detected change, but path does not exist logically. Not triggering callback.",
full
)
else
self.callbacks.change(self.path)
end
end
function PathWatcher:add()
if not self.removed then
-- already added
return
end
log_path.info("adding path %s", self.path)
self.removed = false
self.callbacks.add(self.path)
end
function PathWatcher:remove()
if self.removed then
-- already removed
return
end
log_path.debug("removing path %s", self.path)
log_path.info("path %s was removed, stopping watcher.", self.path)
self.removed = true
self.callbacks.remove(self.path)
-- Would have to re-register for new file to receive new notifications.
self:stop()
end
function PathWatcher:fs_event_callback(err, relpath, events)
if not self.send_notifications then
return
end
vim.schedule_wrap(function()
log_path.debug(
"raw: path: %s; err: %s; relpath: %s; change: %s; rename: %s",
self.path,
err,
relpath,
events.change,
events.rename
)
if events.rename then
if not uv.fs_stat(self.path) then
self:remove()
else
self:add()
end
elseif events.change then
self:change()
end
end)()
end
function PathWatcher:BufWritePost_callback(realpath)
if not self.send_notifications then
return
end
if realpath == self.realpath then
-- notify using passed path, not realpath.
self:change(self.path)
end
end
function PathWatcher:start()
if self.stopped then
-- stop() prevents start.
return
end
self.send_notifications = true
if self.fs_event_providers.libuv then
-- does not work on nfs-drive, at least if it's edited from another
-- machine.
local success, err = self.fs_event:start(
self.path,
{},
function(err, relpath, events)
self:fs_event_callback(err, relpath, events)
end
)
if not success then
log_path.error(
"Could not start libuv-monitor for file %s due to error %s",
self.path,
err
)
else
log_path.info("Monitoring file %s with libuv-monitor.", self.path)
end
end
local realpath = Path.normalize(self.path)
if self.fs_event_providers.autocmd then
if realpath then
self.realpath = realpath
-- path exists, add file-monitor.
table.insert(M.autocmd_watchers, self)
log_path.info("Monitoring file %s with autocmd-monitor.", self.path)
else
log_path.error(
"Could not resolve realpath for file %s, not enabling BufWritePost-monitor",
self.path
)
end
end
if realpath then
table.insert(M.active_watchers, self)
end
if realpath then
-- path exists, notify.
self:add()
-- no else, never added the path, never call remove.
end
end
function PathWatcher:stop()
-- don't check which fs_event_providers were actually started, for both of
-- these it should not matter if they weren't.
self.stopped = true
self.send_notifications = false
self.fs_event:stop()
end
--- Set up new watcher on a single path only.
--- @param path string Absolute path to the root.
--- @param callbacks LuaSnip.FSWatcher.PathCallbacks The callbacks to use for this watcher.
--- @param opts LuaSnip.FSWatcher.Options? Options, described in their class.
--- @return LuaSnip.FSWatcher.Path
function M.path(path, callbacks, opts)
local lazy, fs_event_providers = get_opts(opts)
-- do nothing on missing callback.
callbacks = setmetatable(callbacks or {}, callback_mt)
--- @as LuaSnip.FSWatcher.Path
local o = setmetatable({
path = path,
fs_event = uv.new_fs_event(),
-- Don't send an initial remove-callback if the path does not yet
-- exist.
-- Always send add first, or send nothing.
removed = true,
-- these two are just like in TreeWatcher.
stopped = false,
-- wait for `start()` to send notifications.
send_notifications = false,
callbacks = callbacks,
fs_event_providers = fs_event_providers,
}, PathWatcher_mt)
-- if the path does not yet exist, set watcher up s.t. it will start
-- watching when the directory is created.
if not uv.fs_stat(path) and lazy then
-- root does not yet exist, need to create a watcher that notifies us
-- of its creation.
local parent_path = Path.parent(path)
if not parent_path then
error(("Could not find parent-path for %s"):format(path))
end
log_path.info(
"Path %s does not exist yet, watching %s for creation.",
path,
parent_path
)
local parent_watcher
parent_watcher = M.tree(parent_path, 1, {
-- in path_watcher, watch for new file.
new_file = function(full)
log_path.info("Path: %s %s", full, path)
if full == path then
o:start()
-- directory was created, stop watching.
parent_watcher:stop_self()
end
end,
}, { lazy = true, fs_event_providers = fs_event_providers })
else
o:start()
end
return o
end
return M

View File

@ -0,0 +1,158 @@
local util = require("luasnip.util.util")
local Path = require("luasnip.util.path")
local loader_util = require("luasnip.loaders.util")
local session = require("luasnip.session")
local loader_data = require("luasnip.loaders.data")
local fs_watchers = require("luasnip.loaders.fs_watchers")
local log = require("luasnip.util.log").new("loader")
local M = {}
-- used to map cache-name to name passed to format.
local clean_name = {
vscode_packages = "vscode",
vscode_standalone = "vscode-standalone",
snipmate = "snipmate",
lua = "lua",
}
local function default_format(path, _)
path = path:gsub(
vim.pesc(vim.fn.stdpath("data") .. "/site/pack/packer/start"),
"$PLUGINS"
)
if vim.env.HOME then
path = path:gsub(vim.pesc(vim.env.HOME .. "/.config/nvim"), "$CONFIG")
end
return path
end
local function default_edit(file)
vim.cmd("edit " .. file)
end
--- Quickly jump to snippet-file from any source for the active filetypes.
---@param opts nil|table, options for this function:
--- - ft_filter: fn(filetype:string) -> bool
--- Optionally filter filetypes which can be picked from. `true` -> filetype
--- is listed, `false` -> not listed.
---
--- - format: fn(path:string, source_name:string) -> string|nil
--- source_name is one of "vscode", "snipmate" or "lua".
--- May be used to format the displayed items. For example, replace the
--- excessively long packer-path with something shorter.
--- If format returns nil for some item, the item will not be displayed.
---
--- - edit: fn(file:string): this function is called with the snippet-file as
--- the lone argument.
--- The default is a function which just calls `vim.cmd("edit " .. file)`.
function M.edit_snippet_files(opts)
opts = opts or {}
local format = opts.format or default_format
local edit = opts.edit or default_edit
local extend = opts.extend or function(_, _)
return {}
end
local function ft_edit_picker(ft, _)
if ft then
local ft_paths = {}
local items = {}
-- concat files from all loaders for the selected filetype ft.
for cache_name, ft_file_set in pairs({
vscode_packages = loader_data.vscode_ft_paths[ft],
vscode_standalone = {},
snipmate = loader_data.snipmate_ft_paths[ft],
lua = loader_data.lua_ft_paths[ft],
}) do
for path, _ in pairs(ft_file_set or {}) do
local fmt_name = format(path, clean_name[cache_name])
if fmt_name then
table.insert(ft_paths, path)
table.insert(items, fmt_name)
end
end
end
-- extend filetypes with user-defined function.
local extended = extend(ft, ft_paths)
assert(
type(extended) == "table",
"You must return a table in extend function"
)
for _, pair in ipairs(extended) do
table.insert(items, pair[1])
table.insert(ft_paths, pair[2])
end
-- prompt user again if there are multiple files providing this filetype.
if #ft_paths > 1 then
vim.ui.select(items, {
prompt = "Multiple files for this filetype, choose one:",
}, function(_, indx)
if indx and ft_paths[indx] then
edit(ft_paths[indx])
end
end)
elseif ft_paths[1] then
edit(ft_paths[1])
end
end
end
local ft_filter = opts.ft_filter or util.yes
local all_fts = {}
vim.list_extend(all_fts, util.get_snippet_filetypes())
vim.list_extend(
all_fts,
loader_util.get_load_fts(vim.api.nvim_get_current_buf())
)
all_fts = util.deduplicate(all_fts)
local filtered_fts = {}
for _, ft in ipairs(all_fts) do
if ft_filter(ft) then
table.insert(filtered_fts, ft)
end
end
if #filtered_fts == 1 then
ft_edit_picker(filtered_fts[1])
elseif #filtered_fts > 1 then
vim.ui.select(filtered_fts, {
prompt = "Select filetype:",
}, ft_edit_picker)
end
end
function M.cleanup()
require("luasnip.loaders.from_lua").clean()
require("luasnip.loaders.from_snipmate").clean()
require("luasnip.loaders.from_vscode").clean()
end
function M.load_lazy_loaded(bufnr)
local fts = loader_util.get_load_fts(bufnr)
for _, ft in ipairs(fts) do
if not session.loaded_fts[ft] then
require("luasnip.loaders.from_lua")._load_lazy_loaded_ft(ft)
require("luasnip.loaders.from_snipmate")._load_lazy_loaded_ft(ft)
require("luasnip.loaders.from_vscode")._load_lazy_loaded_ft(ft)
end
session.loaded_fts[ft] = true
end
end
function M.reload_file(path)
local realpath = Path.normalize(path)
if not realpath then
return nil, ("Could not reload file %s: does not exist."):format(path)
else
fs_watchers.write_notify(realpath)
return true
end
end
return M

View File

@ -0,0 +1,93 @@
local uv = vim.uv or vim.loop
local duplicate = require("luasnip.nodes.duplicate")
--- @class LuaSnip.Loaders.SnippetCache.Mtime
--- @field sec number
--- @field nsec number
--- Stores modified time for a file.
--- @class LuaSnip.Loaders.SnippetCache.TimeCacheEntry
--- @field mtime LuaSnip.Loaders.SnippetCache.Mtime?
--- @field data LuaSnip.Loaders.SnippetFileData
--- mtime is nil if the file does not currently exist. Since `get_fn` may still
--- return data, there's no need to treat this differently.
--- @class LuaSnip.Loaders.SnippetCache
--- SnippetCache stores snippets and other data loaded by files.
--- @field private get_fn fun(file: string): LuaSnip.Loaders.SnippetFileData
--- @field private cache table<string, LuaSnip.Loaders.SnippetCache.TimeCacheEntry>
local SnippetCache = {}
SnippetCache.__index = SnippetCache
local M = {}
--- @class LuaSnip.Loaders.SnippetFileData
--- @field snippets LuaSnip.Addable[]
--- @field autosnippets LuaSnip.Addable[]
--- @field misc table any data.
--- Create new cache.
--- @param get_fn fun(file: string): LuaSnip.Loaders.SnippetFileData
--- @return LuaSnip.Loaders.SnippetCache
function M.new(get_fn)
return setmetatable({
get_fn = get_fn,
cache = {},
}, SnippetCache)
end
--- Copy addables from data to new table.
--- @param data LuaSnip.Loaders.SnippetFileData
--- @return LuaSnip.Loaders.SnippetFileData
local function copy_filedata(data)
--- @as LuaSnip.Loaders.SnippetFileData
return {
snippets = vim.tbl_map(duplicate.duplicate_addable, data.snippets),
autosnippets = vim.tbl_map(
duplicate.duplicate_addable,
data.autosnippets
),
misc = vim.deepcopy(data.misc),
}
end
--- Retrieve loaded data for any file, either from the cache, or directly from
--- the file.
--- For storage-efficiency (and to elide the otherwise necessary deepcopy), the
--- snippets are duplicated, which should not leak.
--- @param fname string
--- @return LuaSnip.Loaders.SnippetFileData
function SnippetCache:fetch(fname)
local cached = self.cache[fname]
local current_stat = uv.fs_stat(fname)
--- @as LuaSnip.Loaders.SnippetCache.Mtime
local mtime = current_stat and current_stat.mtime
if
cached
and mtime
and mtime.sec == cached.mtime.sec
and mtime.nsec == cached.mtime.nsec
then
-- happy path: data is cached, and valid => just return cached data.
return copy_filedata(cached.data)
end
-- data is stale (cache entry does not exist, file was written after
-- cache-creation, or the file was deleted).
-- fetch data from updated file
local res = self.get_fn(fname)
-- store it.
self.cache[fname] = {
data = res,
mtime = mtime,
}
-- return it.
-- Don't copy here, no need to.
return res
end
return M

View File

@ -0,0 +1,8 @@
--- @class LuaSnip.Loaders.LoadOpts
--- @field paths string[]?|string? Either a list of paths, or ","-delimited paths. If nil, searches rtp for snippet-collections.
--- @field lazy_paths string[]?|string? Like paths, but these will be watched, and loaded when creation is detected.
--- @field include string[]? If set, all filetypes not in include will be excluded from loading.
--- @field exclude string[]? Exclude these filetypes, even if they are set in include.
--- @field override_priority number? load all snippets with this priority.
--- @field default_priority number? snippet-priority, unless the snippet sets its own priority.
--- @field fs_event_providers table<LuaSnip.FSWatcher.FSEventProviders, boolean>? How to monitor the filesystem

View File

@ -0,0 +1,299 @@
local Path = require("luasnip.util.path")
local util = require("luasnip.util.util")
local session = require("luasnip.session")
local snippet_collection = require("luasnip.session.snippet_collection")
local log = require("luasnip.util.log").new("loaders")
local function filetypelist_to_set(list)
vim.validate({ list = { list, "table", true } })
if not list then
return list
end
local out = {}
for _, ft in ipairs(list) do
out[ft] = true
end
return out
end
local function split_lines(filestring)
local newline_code
if vim.endswith(filestring, "\r\n") then -- dos
newline_code = "\r\n"
elseif vim.endswith(filestring, "\r") then -- mac
-- both mac and unix-files contain a trailing newline which would lead
-- to an additional empty line being read (\r, \n _terminate_ lines, they
-- don't _separate_ them)
newline_code = "\r"
filestring = filestring:sub(1, -2)
elseif vim.endswith(filestring, "\n") then -- unix
newline_code = "\n"
filestring = filestring:sub(1, -2)
else -- dos
newline_code = "\r\n"
end
return vim.split(
filestring,
newline_code,
{ plain = true, trimemtpy = false }
)
end
local function non_nil(v)
return v ~= nil
end
local function resolve_root_paths(paths, rtp_dirname)
if not paths then
paths = vim.api.nvim_get_runtime_file(rtp_dirname, true)
end
paths = vim.tbl_map(Path.expand, paths)
paths = vim.tbl_filter(non_nil, paths)
paths = util.deduplicate(paths)
return paths
end
local function resolve_lazy_root_paths(paths)
paths = vim.tbl_map(Path.expand_maybe_nonexisting, paths)
paths = vim.tbl_filter(non_nil, paths)
paths = util.deduplicate(paths)
return paths
end
local function ft_filter(include, exclude)
exclude = filetypelist_to_set(exclude)
include = filetypelist_to_set(include)
return function(lang)
if exclude and exclude[lang] then
return false
end
if include == nil or include[lang] then
return true
end
end
end
local function _append(tbl, name, elem)
if tbl[name] == nil then
tbl[name] = {}
end
table.insert(tbl[name], elem)
end
---Get paths of .snippets files
---@param root string @snippet directory path
---@return table @keys are file types, values are paths
local function get_ft_paths(root, extension)
local ft_path = {}
local files, dirs = Path.scandir(root)
for _, file in ipairs(files) do
local ft, ext = Path.basename(file, true)
if ext == extension then
_append(ft_path, ft, file)
end
end
for _, dir in ipairs(dirs) do
-- directory-name is ft for snippet-files.
local ft = vim.fn.fnamemodify(dir, ":t")
files, _ = Path.scandir(dir)
for _, file in ipairs(files) do
if vim.endswith(file, extension) then
-- produce normalized filenames.
local normalized_fname = Path.normalize(file)
if normalized_fname then
_append(ft_path, ft, normalized_fname)
end
end
end
end
return ft_path
end
-- fname must be in the directory-tree below root.
-- collection_root may not end with a path-separator.
-- If both are from "realpath", and fname belongs to the collection, this
-- should be a given.
local function collection_file_ft(collection_root, fname)
local collection_components = Path.components(collection_root)
local fname_components = Path.components(fname)
if #fname_components == #collection_components + 1 then
-- if the file is a direct child of the collection-root, get the text
-- before the last dot.
return fname_components[#collection_components + 1]:match(
"(.*)%.[^%.]*"
)
else
-- if the file is nested deeper, the name of the directory immediately
-- below the root is the filetype.
return fname_components[#collection_components + 1]
end
end
-- extend table like {lua = {path1}, c = {path1, path2}, ...}, new_paths has the same layout.
local function extend_ft_paths(paths, new_paths)
for ft, path in pairs(new_paths) do
if paths[ft] then
vim.list_extend(paths[ft], path)
else
paths[ft] = vim.deepcopy(path)
end
end
end
--- Find
--- 1. all files that belong to a collection and
--- 2. the files from that
--- collection that should actually be loaded.
---@param opts table: straight from `load`/`lazy_load`.
---@param rtp_dirname string: if no path is given in opts, we look for a
--- directory named `rtp_dirname` in the runtimepath.
---@param extension string: extension of valid snippet-files for the given
--- collection (eg `.lua` or `.snippets`)
---@return table: a list of tables, each of the inner tables contains two
--- entries:
--- - collection_paths: ft->files for the entire collection and
--- - load_paths: ft->files for only the files that should be loaded.
--- All produced filenames are normalized, eg. links are resolved and
--- unnecessary . or .. removed.
local function get_load_paths_snipmate_like(opts, rtp_dirname, extension)
local collections_load_paths = {}
for _, path in ipairs(resolve_root_paths(opts.paths, rtp_dirname)) do
local collection_ft_paths = get_ft_paths(path, extension)
local load_paths = vim.deepcopy(collection_ft_paths)
-- remove files for excluded/non-included filetypes here.
local collection_filter = ft_filter(opts.exclude, opts.include)
for ft, _ in pairs(load_paths) do
if not collection_filter(ft) then
load_paths[ft] = nil
end
end
table.insert(collections_load_paths, {
collection_paths = collection_ft_paths,
load_paths = load_paths,
})
end
return collections_load_paths
end
--- Asks (via vim.ui.select) to edit a file that currently provides snippets
---@param ft_files table, map filetype to a number of files.
local function edit_snippet_files(ft_files)
local fts = util.get_snippet_filetypes()
vim.ui.select(fts, {
prompt = "Select filetype:",
}, function(item, _)
if item then
local ft_paths = ft_files[item]
if ft_paths then
-- prompt user again if there are multiple files providing this filetype.
if #ft_paths > 1 then
vim.ui.select(ft_paths, {
prompt = "Multiple files for this filetype, choose one:",
}, function(multi_item)
if multi_item then
vim.cmd("edit " .. multi_item)
end
end)
else
vim.cmd("edit " .. ft_paths[1])
end
else
print("No file for this filetype.")
end
end
end)
end
local function make_add_opts(opts)
return {
override_priority = opts.override_priority,
default_priority = opts.default_priority,
}
end
local function get_load_fts(bufnr)
local fts = session.config.load_ft_func(bufnr)
-- also add "all", loaded by all buffers.
table.insert(fts, "all")
return util.deduplicate(util.redirect_filetypes(fts))
end
local function add_file_snippets(ft, filename, snippets, autosnippets, add_opts)
snippet_collection.add_snippets(
{ [ft] = snippets },
vim.tbl_extend("keep", {
type = "snippets",
key = "__snippets__" .. ft .. "__" .. filename,
}, add_opts)
)
snippet_collection.add_snippets(
{ [ft] = autosnippets },
vim.tbl_extend("keep", {
type = "autosnippets",
key = "__autosnippets__" .. ft .. "__" .. filename,
}, add_opts)
)
log.info(
"Adding %s snippets and %s autosnippets from %s to ft `%s`",
#snippets,
#autosnippets,
filename,
ft
)
end
local function normalize_opts(opts)
opts = opts or {}
local paths = opts.paths
if type(paths) == "string" then
paths = vim.split(paths, ",")
end
local add_opts = make_add_opts(opts)
local include = opts.include
local exclude = opts.exclude
local lazy_paths = opts.lazy_paths or {}
if type(lazy_paths) == "string" then
lazy_paths = vim.split(lazy_paths, ",")
end
local fs_event_providers =
vim.F.if_nil(opts.fs_event_providers, { autocmd = true, libuv = false })
return {
paths = paths,
lazy_paths = lazy_paths,
include = include,
exclude = exclude,
add_opts = add_opts,
fs_event_providers = fs_event_providers,
}
end
return {
filetypelist_to_set = filetypelist_to_set,
split_lines = split_lines,
resolve_root_paths = resolve_root_paths,
resolve_lazy_root_paths = resolve_lazy_root_paths,
ft_filter = ft_filter,
get_ft_paths = get_ft_paths,
get_load_paths_snipmate_like = get_load_paths_snipmate_like,
extend_ft_paths = extend_ft_paths,
edit_snippet_files = edit_snippet_files,
make_add_opts = make_add_opts,
collection_file_ft = collection_file_ft,
get_load_fts = get_load_fts,
add_file_snippets = add_file_snippets,
normalize_opts = normalize_opts,
}

View File

@ -0,0 +1,27 @@
-- absolute_indexer[0][1][2][3] -> { absolute_insert_position = {0,1,2,3} }
local function new()
return setmetatable({
absolute_insert_position = {},
}, {
__index = function(table, key)
table.absolute_insert_position[#table.absolute_insert_position + 1] =
key
return table
end,
})
end
return setmetatable({}, {
__index = function(_, key)
-- create new table and index it.
return new()[key]
end,
__call = function(_, ...)
return {
-- passing ... to a function passes only the first of the
-- variable number of args.
absolute_insert_position = type(...) == "number" and { ... } or ...,
}
end,
})

View File

@ -0,0 +1,427 @@
local Node = require("luasnip.nodes.node").Node
local ChoiceNode = Node:new()
local util = require("luasnip.util.util")
local node_util = require("luasnip.nodes.util")
local types = require("luasnip.util.types")
local events = require("luasnip.util.events")
local mark = require("luasnip.util.mark").mark
local session = require("luasnip.session")
local sNode = require("luasnip.nodes.snippet").SN
local extend_decorator = require("luasnip.util.extend_decorator")
function ChoiceNode:init_nodes()
for i, choice in ipairs(self.choices) do
-- setup jumps
choice.next = self
choice.prev = self
-- forward values for unknown keys from choiceNode.
choice.choice = self
local node_mt = getmetatable(choice)
setmetatable(choice, {
__index = function(node, key)
return node_mt[key] or node.choice[key]
end,
})
-- replace nodes' original update_dependents with function that also
-- calls this choiceNodes' update_dependents.
--
-- cannot define as `function node:update_dependents()` as _this_
-- choiceNode would be `self`.
-- Also rely on node.choice, as using `self` there wouldn't be caught
-- by copy and the wrong node would be updated.
choice.update_dependents = function(node)
node:_update_dependents()
node.choice:update_dependents()
end
choice.next_choice = self.choices[i + 1]
choice.prev_choice = self.choices[i - 1]
end
self.choices[#self.choices].next_choice = self.choices[1]
self.choices[1].prev_choice = self.choices[#self.choices]
self.active_choice = self.choices[1]
end
local function C(pos, choices, opts)
opts = opts or {}
if opts.restore_cursor == nil then
-- disable by default, can affect performance.
opts.restore_cursor = false
end
-- allow passing table of nodes in choices, will be turned into a
-- snippetNode.
for indx, choice in ipairs(choices) do
if not getmetatable(choice) then
-- is a normal table, not a node.
choices[indx] = sNode(nil, choice)
end
end
local c = ChoiceNode:new({
active = false,
pos = pos,
choices = choices,
type = types.choiceNode,
mark = nil,
dependents = {},
-- default to true.
restore_cursor = opts.restore_cursor,
}, opts)
c:init_nodes()
return c
end
extend_decorator.register(C, { arg_indx = 3 })
function ChoiceNode:subsnip_init()
node_util.subsnip_init_children(self.parent, self.choices)
end
ChoiceNode.init_positions = node_util.init_child_positions_func(
"absolute_position",
"choices",
"init_positions"
)
ChoiceNode.init_insert_positions = node_util.init_child_positions_func(
"absolute_insert_position",
"choices",
"init_insert_positions"
)
function ChoiceNode:make_args_absolute()
-- relative indices are relative to the parent of the choiceNode,
-- temporarily remove last component of position
local last_indx = #self.absolute_insert_position
local last = self.absolute_insert_position[last_indx]
self.absolute_insert_position[#self.absolute_insert_position] = nil
for _, choice in ipairs(self.choices) do
-- relative to choiceNode!!
choice:make_args_absolute(self.absolute_insert_position)
end
self.absolute_insert_position[last_indx] = last
end
function ChoiceNode:put_initial(pos)
local old_pos = vim.deepcopy(pos)
self.active_choice:put_initial(pos)
local mark_opts = vim.tbl_extend("keep", {
right_gravity = false,
end_right_gravity = false,
}, self.active_choice:get_passive_ext_opts())
self.active_choice.mark = mark(old_pos, pos, mark_opts)
self.visible = true
end
function ChoiceNode:indent(indentstr)
for _, node in ipairs(self.choices) do
node:indent(indentstr)
end
end
function ChoiceNode:expand_tabs(tabwidth, indentstringlen)
for _, node in ipairs(self.choices) do
node:expand_tabs(tabwidth, indentstringlen)
end
end
function ChoiceNode:input_enter(_, dry_run)
if dry_run then
dry_run.active[self] = true
return
end
self.mark:update_opts(self.ext_opts.active)
self:focus()
self.prev_choice_node =
session.active_choice_nodes[vim.api.nvim_get_current_buf()]
session.active_choice_nodes[vim.api.nvim_get_current_buf()] = self
self.visited = true
self.active = true
self:event(events.enter)
end
function ChoiceNode:input_leave(_, dry_run)
if dry_run then
dry_run.active[self] = false
return
end
self:event(events.leave)
self.mark:update_opts(self:get_passive_ext_opts())
self:update_dependents()
session.active_choice_nodes[vim.api.nvim_get_current_buf()] =
self.prev_choice_node
self.active = false
end
function ChoiceNode:set_old_text()
self.old_text = self:get_text()
self.active_choice.old_text = self.old_text
end
function ChoiceNode:get_static_text()
return self.choices[1]:get_static_text()
end
function ChoiceNode:get_docstring()
return util.string_wrap(
self.choices[1]:get_docstring(),
rawget(self, "pos")
)
end
function ChoiceNode:jump_into(dir, no_move, dry_run)
self:init_dry_run_active(dry_run)
if self:is_active(dry_run) then
self:input_leave(no_move, dry_run)
if dir == 1 then
return self.next:jump_into(dir, no_move, dry_run)
else
return self.prev:jump_into(dir, no_move, dry_run)
end
else
self:input_enter(no_move, dry_run)
return self.active_choice:jump_into(dir, no_move, dry_run)
end
end
function ChoiceNode:update()
self.active_choice:update()
end
function ChoiceNode:update_static_all()
for _, choice in ipairs(self.choices) do
choice:update_static()
end
end
function ChoiceNode:update_static()
self.active_choice:update_static()
end
function ChoiceNode:update_restore()
self.active_choice:update_restore()
end
function ChoiceNode:setup_choice_jumps() end
function ChoiceNode:find_node(predicate)
if self.active_choice then
if predicate(self.active_choice) then
return self.active_choice
else
return self.active_choice:find_node(predicate)
end
end
return nil
end
-- used to uniquely identify this change-choice-action.
local change_choice_id = 0
function ChoiceNode:set_choice(choice, current_node)
change_choice_id = change_choice_id + 1
-- to uniquely identify this node later (storing the pointer isn't enough
-- because this is supposed to work with restoreNodes, which are copied).
current_node.change_choice_id = change_choice_id
local insert_pre_cc = vim.fn.mode() == "i"
-- is byte-indexed! Doesn't matter here, but important to be aware of.
local cursor_pos_pre_relative =
util.pos_sub(util.get_cursor_0ind(), current_node.mark:pos_begin_raw())
self.active_choice:store()
-- tear down current choice.
-- leave all so the choice (could be a snippet) is in the correct state for the next enter.
node_util.leave_nodes_between(self.active_choice, current_node)
self.active_choice:exit()
-- clear text.
--
-- active_choice has to be disabled (nilled?) to prevent reading from
-- cleared mark in set_mark_rgrav (which will be called in
-- self:set_text({""}) a few lines below).
self.active_choice = nil
self:set_text({ "" })
self.active_choice = choice
self.active_choice.mark = self.mark:copy_pos_gravs(
vim.deepcopy(self.active_choice:get_passive_ext_opts())
)
-- re-init positions for child-restoreNodes (they will update their
-- children in put_initial, but their own position has to be changed here).
self:init_positions(self.absolute_position)
self:init_insert_positions(self.absolute_insert_position)
-- self is still focused, from `set_text`.
self.active_choice:put_initial(self.mark:pos_begin_raw())
-- adjust gravity in left side of inserted node, such that it matches the
-- current gravity of self.
local _, to = self.mark:pos_begin_end_raw()
self.active_choice:subtree_set_pos_rgrav(to, -1, true)
self.active_choice:update_restore()
self.active_choice:update_all_dependents()
self:update_dependents()
-- Another node may have been entered in update_dependents.
self:focus()
self:event(events.change_choice)
if self.restore_cursor then
local target_node = self:find_node(function(test_node)
return test_node.change_choice_id == change_choice_id
end)
if target_node then
-- the node that the cursor was in when changeChoice was called exists
-- in the active choice! Enter it and all nodes between it and this choiceNode,
-- then set the cursor.
-- Pass no_move=true, we will set the cursor ourselves.
node_util.enter_nodes_between(self, target_node, true)
if insert_pre_cc then
util.set_cursor_0ind(
util.pos_add(
target_node.mark:pos_begin_raw(),
cursor_pos_pre_relative
)
)
else
node_util.select_node(target_node)
end
return target_node
end
end
return self.active_choice:jump_into(1)
end
function ChoiceNode:change_choice(dir, current_node)
-- stylua: ignore
return self:set_choice(
dir == 1 and self.active_choice.next_choice
or self.active_choice.prev_choice,
current_node )
end
function ChoiceNode:copy()
local o = vim.deepcopy(self)
for i, node in ipairs(self.choices) do
if node.type == types.snippetNode or node.type == types.choiceNode then
o.choices[i] = node:copy()
else
setmetatable(o.choices[i], getmetatable(node))
end
end
setmetatable(o, getmetatable(self))
return o
end
function ChoiceNode:exit()
self.visible = false
if self.active_choice then
self.active_choice:exit()
end
self.mark:clear()
if self.active then
session.active_choice_nodes[vim.api.nvim_get_current_buf()] =
self.prev_choice_node
end
self.active = false
end
function ChoiceNode:set_ext_opts(name)
Node.set_ext_opts(self, name)
self.active_choice:set_ext_opts(name)
end
function ChoiceNode:store()
self.active_choice:store()
end
function ChoiceNode:insert_to_node_absolute(position)
if #position == 0 then
return self.absolute_position
end
local front = util.pop_front(position)
return self.choices[front]:insert_to_node_absolute(position)
end
function ChoiceNode:set_dependents()
for _, node in ipairs(self.choices) do
node:set_dependents()
end
end
function ChoiceNode:set_argnodes(dict)
Node.set_argnodes(self, dict)
for _, node in ipairs(self.choices) do
node:set_argnodes(dict)
end
end
function ChoiceNode:update_all_dependents()
-- call the version that only updates this node.
self:_update_dependents()
self.active_choice:update_all_dependents()
end
function ChoiceNode:update_all_dependents_static()
-- call the version that only updates this node.
self:_update_dependents_static()
self.active_choice:update_all_dependents_static()
end
function ChoiceNode:resolve_position(position)
return self.choices[position]
end
function ChoiceNode:static_init()
Node.static_init(self)
self.active_choice:static_init()
end
function ChoiceNode:subtree_set_pos_rgrav(pos, direction, rgrav)
self.mark:set_rgrav(-direction, rgrav)
if self.active_choice then
self.active_choice:subtree_set_pos_rgrav(pos, direction, rgrav)
end
end
function ChoiceNode:subtree_set_rgrav(rgrav)
self.mark:set_rgravs(rgrav, rgrav)
if self.active_choice then
self.active_choice:subtree_set_rgrav(rgrav)
end
end
function ChoiceNode:extmarks_valid()
return node_util.generic_extmarks_valid(self, self.active_choice)
end
return {
C = C,
}

View File

@ -0,0 +1,73 @@
local snip_mod = require("luasnip.nodes.snippet")
local M = {}
local DupExpandable = {}
-- just pass these through to _expandable.
function DupExpandable:get_docstring()
return self._expandable:get_docstring()
end
function DupExpandable:copy()
local copy = self._expandable:copy()
copy.id = self.id
return copy
end
-- this is modified in `self:invalidate` _and_ needs to be called on _expandable.
function DupExpandable:matches(...)
-- use snippet-module matches, self._expandable might have had its match
-- overwritten by invalidate.
-- (if there are more issues with this, consider some other mechanism for
-- invalidating)
return snip_mod.Snippet.matches(self._expandable, ...)
end
-- invalidate has to be called on this snippet itself.
function DupExpandable:invalidate()
snip_mod.Snippet.invalidate(self)
end
local dup_mt = {
-- index DupExpandable for own functions, and then the expandable stored in
-- self/t.
__index = function(t, k)
if DupExpandable[k] then
return DupExpandable[k]
end
return t._expandable[k]
end,
}
function M.duplicate_expandable(expandable)
return setmetatable({
_expandable = expandable,
-- copy these!
-- if `expandable` is invalidated, we don't necessarily want this
-- expandable to be invalidated as well.
hidden = expandable.hidden,
invalidated = expandable.invalidated,
}, dup_mt)
end
local DupAddable = {}
function DupAddable:retrieve_all()
-- always return the same set of items, necessary when for invalidate via
-- key to work correctly.
return self._all
end
local DupAddable_mt = {
__index = DupAddable,
}
function M.duplicate_addable(addable)
return setmetatable({
addable = addable,
_all = vim.tbl_map(M.duplicate_expandable, addable:retrieve_all()),
}, DupAddable_mt)
end
return M

View File

@ -0,0 +1,439 @@
local DynamicNode = require("luasnip.nodes.node").Node:new()
local util = require("luasnip.util.util")
local node_util = require("luasnip.nodes.util")
local Node = require("luasnip.nodes.node").Node
local types = require("luasnip.util.types")
local events = require("luasnip.util.events")
local FunctionNode = require("luasnip.nodes.functionNode").FunctionNode
local SnippetNode = require("luasnip.nodes.snippet").SN
local extend_decorator = require("luasnip.util.extend_decorator")
local function D(pos, fn, args, opts)
opts = opts or {}
return DynamicNode:new({
pos = pos,
fn = fn,
args = node_util.wrap_args(args),
type = types.dynamicNode,
mark = nil,
user_args = opts.user_args or {},
dependents = {},
active = false,
}, opts)
end
extend_decorator.register(D, { arg_indx = 4 })
function DynamicNode:input_enter(_, dry_run)
if dry_run then
dry_run.active[self] = true
return
end
self.visited = true
self.active = true
self.mark:update_opts(self.ext_opts.active)
self:event(events.enter)
end
function DynamicNode:input_leave(_, dry_run)
if dry_run then
dry_run.active[self] = false
return
end
self:event(events.leave)
self:update_dependents()
self.active = false
self.mark:update_opts(self:get_passive_ext_opts())
end
function DynamicNode:get_static_text()
if self.static_snip then
return self.static_snip:get_static_text()
else
self:update_static()
if self.static_snip then
return self.static_snip:get_static_text()
else
return { "" }
end
end
end
function DynamicNode:get_docstring()
if not self.docstring then
if self.static_snip then
self.docstring = self.static_snip:get_docstring()
else
self.docstring = { "" }
end
end
return self.docstring
end
-- DynamicNode's don't have static text, only set as visible.
function DynamicNode:put_initial(_)
self.visible = true
end
function DynamicNode:indent(_) end
function DynamicNode:expand_tabs(_) end
function DynamicNode:jump_into(dir, no_move, dry_run)
-- init dry_run-state for this node.
self:init_dry_run_active(dry_run)
if self:is_active(dry_run) then
self:input_leave(no_move, dry_run)
if dir == 1 then
return self.next:jump_into(dir, no_move, dry_run)
else
return self.prev:jump_into(dir, no_move, dry_run)
end
else
self:input_enter(no_move, dry_run)
if self.snip then
return self.snip:jump_into(dir, no_move, dry_run)
else
-- this will immediately enter and leave, but IMO that's expected
-- behaviour.
self:input_leave(no_move, dry_run)
if dir == 1 then
return self.next:jump_into(dir, no_move, dry_run)
else
return self.prev:jump_into(dir, no_move, dry_run)
end
end
end
end
function DynamicNode:update()
local args = self:get_args()
if vim.deep_equal(self.last_args, args) then
-- no update, the args still match.
return
end
if not self.parent.snippet:extmarks_valid() then
error("Refusing to update inside a snippet with invalid extmarks")
end
local tmp
if self.snip then
if not args then
-- a snippet exists, don't delete it.
return
end
-- build new snippet before exiting, markers may be needed for construncting.
tmp = self.fn(
args,
self.parent,
self.snip.old_state,
unpack(self.user_args)
)
self.snip:exit()
self.snip = nil
-- focuses node.
self:set_text({ "" })
else
self:focus()
if not args then
-- no snippet exists, set an empty one.
tmp = SnippetNode(nil, {})
else
-- also enter node here.
tmp = self.fn(args, self.parent, nil, unpack(self.user_args))
end
end
self.last_args = args
-- act as if snip is directly inside parent.
tmp.parent = self.parent
tmp.indx = self.indx
tmp.next = self
tmp.prev = self
tmp.snippet = self.parent.snippet
tmp:resolve_child_ext_opts()
tmp:resolve_node_ext_opts()
tmp:subsnip_init()
tmp.mark =
self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts()))
tmp.dynamicNode = self
tmp.update_dependents = function(node)
node:_update_dependents()
node.dynamicNode:update_dependents()
end
tmp:init_positions(self.snip_absolute_position)
tmp:init_insert_positions(self.snip_absolute_insert_position)
tmp:make_args_absolute()
tmp:set_dependents()
tmp:set_argnodes(self.parent.snippet.dependents_dict)
if vim.bo.expandtab then
tmp:expand_tabs(util.tab_width(), #self.parent.indentstr)
end
tmp:indent(self.parent.indentstr)
-- sets own extmarks false,true
self:focus()
local from, to = self.mark:pos_begin_end_raw()
-- inserts nodes with extmarks false,false
tmp:put_initial(from)
-- adjust gravity in left side of snippet, such that it matches the current
-- gravity of self.
tmp:subtree_set_pos_rgrav(to, -1, true)
self.snip = tmp
-- Update, tbh no idea how that could come in handy, but should be done.
-- Both are needed, because
-- - a node could only depend on nodes outside of tmp
-- - a node outside of tmp could depend on one inside of tmp
tmp:update()
tmp:update_all_dependents()
self:update_dependents()
end
local update_errorstring = [[
Error while evaluating dynamicNode@%d for snippet '%s':
%s
:h luasnip-docstring for more info]]
function DynamicNode:update_static()
local args = self:get_static_args()
if vim.deep_equal(self.last_static_args, args) then
-- no update, the args still match.
return
end
local tmp, ok
if self.static_snip then
if not args then
-- a snippet exists, don't delete it.
return
end
-- build new snippet before exiting, markers may be needed for construncting.
ok, tmp = pcall(
self.fn,
args,
self.parent,
self.snip.old_state,
unpack(self.user_args)
)
else
if not args then
-- no snippet exists, set an empty one.
tmp = SnippetNode(nil, {})
else
-- also enter node here.
ok, tmp =
pcall(self.fn, args, self.parent, nil, unpack(self.user_args))
end
end
if not ok then
print(
update_errorstring:format(self.indx, self.parent.snippet.name, tmp)
)
-- set empty snippet on failure
tmp = SnippetNode(nil, {})
end
self.last_static_args = args
-- act as if snip is directly inside parent.
tmp.parent = self.parent
tmp.indx = self.indx
tmp.pos = rawget(self, "pos")
tmp.next = self
tmp.prev = self
-- doesn't matter here, but they'll have to be set.
tmp.ext_opts = self.parent.ext_opts
tmp.snippet = self.parent.snippet
tmp.dynamicNode = self
tmp.update_dependents_static = function(node)
node:_update_dependents_static()
node.dynamicNode:update_dependents_static()
end
tmp:resolve_child_ext_opts()
tmp:resolve_node_ext_opts()
tmp:subsnip_init()
tmp:init_positions(self.snip_absolute_position)
tmp:init_insert_positions(self.snip_absolute_insert_position)
tmp:make_args_absolute()
tmp:set_dependents()
tmp:set_argnodes(self.parent.snippet.dependents_dict)
-- do not expand tabs!! This is only necessary if the snippet is inserted
-- in a buffer, some information is lost if tabs (indent) is replaced with
-- whitespace.
-- This might make a difference when another f/dynamicNode depends on this
-- one, and the function expects expanded tabs... imo the function should
-- be adjusted to accept any whitespace.
tmp:indent(self.parent.indentstr)
tmp:static_init()
tmp:update_static()
-- updates dependents in tmp.
tmp:update_all_dependents_static()
self.static_snip = tmp
-- updates own dependents.
self:update_dependents_static()
end
function DynamicNode:exit()
self.visible = false
self.mark:clear()
-- check if snip actually exists, may not be the case if
-- the surrounding snippet was deleted just before.
if self.snip then
self.snip:exit()
end
self.stored_snip = self.snip
self.snip = nil
self.active = false
end
function DynamicNode:set_ext_opts(name)
Node.set_ext_opts(self, name)
-- might not have been generated (missing nodes).
if self.snip then
self.snip:set_ext_opts(name)
end
end
function DynamicNode:store()
if self.snip then
self.snip:store()
end
end
function DynamicNode:update_restore()
-- only restore snippet if arg-values still match.
if self.stored_snip and vim.deep_equal(self:get_args(), self.last_args) then
local tmp = self.stored_snip
tmp.mark =
self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts()))
-- position might (will probably!!) still have changed, so update it
-- here too (as opposed to only in update).
tmp:init_positions(self.snip_absolute_position)
tmp:init_insert_positions(self.snip_absolute_insert_position)
tmp:make_args_absolute()
tmp:set_dependents()
tmp:set_argnodes(self.parent.snippet.dependents_dict)
-- sets own extmarks false,true
self:focus()
-- inserts nodes with extmarks false,false
local from, to = self.mark:pos_begin_end_raw()
tmp:put_initial(from)
-- adjust gravity in left side of snippet, such that it matches the current
-- gravity of self.
tmp:subtree_set_pos_rgrav(to, -1, true)
-- set snip before update_restore, since update_restore involves
-- calling `focus`, and that needs `snip` to be set.
-- If it is not set, tmp is not reachable via get_nodes_between.
-- (TODO: This is pretty bad, have to rethink design sometime).
self.snip = tmp
tmp:update_restore()
else
self:update()
end
end
function DynamicNode:find_node(predicate)
if self.snip then
if predicate(self.snip) then
return self.snip
else
return self.snip:find_node(predicate)
end
end
return nil
end
function DynamicNode:insert_to_node_absolute(position)
if #position == 0 then
return self.absolute_position
end
return self.snip and self.snip:insert_to_node_absolute(position)
end
function DynamicNode:init_insert_positions(position_so_far)
Node.init_insert_positions(self, position_so_far)
self.snip_absolute_insert_position =
vim.deepcopy(self.absolute_insert_position)
-- nodes of current snippet should have a 0 before.
self.snip_absolute_insert_position[#self.snip_absolute_insert_position + 1] =
0
end
function DynamicNode:init_positions(position_so_far)
Node.init_positions(self, position_so_far)
self.snip_absolute_position = vim.deepcopy(self.absolute_position)
-- Reach current snippet as snip_absolute_position..0.
self.snip_absolute_position[#self.snip_absolute_position + 1] = 0
end
DynamicNode.make_args_absolute = FunctionNode.make_args_absolute
DynamicNode.set_dependents = FunctionNode.set_dependents
function DynamicNode:resolve_position(position)
-- position must be 0, there are no other options.
return self.snip
end
function DynamicNode:subtree_set_pos_rgrav(pos, direction, rgrav)
self.mark:set_rgrav(-direction, rgrav)
if self.snip then
self.snip:subtree_set_pos_rgrav(pos, direction, rgrav)
end
end
function DynamicNode:subtree_set_rgrav(rgrav)
self.mark:set_rgravs(rgrav, rgrav)
if self.snip then
self.snip:subtree_set_rgrav(rgrav)
end
end
function DynamicNode:extmarks_valid()
if self.snip then
return node_util.generic_extmarks_valid(self, self.snip)
end
return true
end
return {
D = D,
}

View File

@ -0,0 +1,155 @@
local Node = require("luasnip.nodes.node").Node
local FunctionNode = Node:new()
local util = require("luasnip.util.util")
local node_util = require("luasnip.nodes.util")
local types = require("luasnip.util.types")
local tNode = require("luasnip.nodes.textNode").textNode
local extend_decorator = require("luasnip.util.extend_decorator")
local key_indexer = require("luasnip.nodes.key_indexer")
local function F(fn, args, opts)
opts = opts or {}
return FunctionNode:new({
fn = fn,
args = node_util.wrap_args(args),
type = types.functionNode,
mark = nil,
user_args = opts.user_args or {},
}, opts)
end
extend_decorator.register(F, { arg_indx = 3 })
FunctionNode.input_enter = tNode.input_enter
function FunctionNode:get_static_text()
-- static_text will already have been generated, if possible.
-- If it isn't generated, prevent errors by just setting it to empty text.
if not self.static_text then
self.static_text = { "" }
end
return self.static_text
end
-- function-text will not stand out in any way in docstring.
FunctionNode.get_docstring = FunctionNode.get_static_text
function FunctionNode:update()
local args = self:get_args()
-- skip this update if
-- - not all nodes are available.
-- - the args haven't changed.
if not args or vim.deep_equal(args, self.last_args) then
return
end
if not self.parent.snippet:extmarks_valid() then
error("Refusing to update inside a snippet with invalid extmarks")
end
self.last_args = args
local text =
util.to_string_table(self.fn(args, self.parent, unpack(self.user_args)))
if vim.bo.expandtab then
util.expand_tabs(text, util.tab_width(), #self.parent.indentstr)
end
-- don't expand tabs in parent.indentstr, use it as-is.
self:set_text(util.indent(text, self.parent.indentstr))
self:update_dependents()
end
local update_errorstring = [[
Error while evaluating functionNode@%d for snippet '%s':
%s
:h luasnip-docstring for more info]]
function FunctionNode:update_static()
local args = self:get_static_args()
-- skip this update if
-- - not all nodes are available.
-- - the args haven't changed.
if not args or vim.deep_equal(args, self.last_args) then
return
end
-- should be okay to set last_args even if `fn` potentially fails, future
-- updates will fail aswell, if not the `fn` also doesn't always work
-- correctly in normal expansion.
self.last_args = args
local ok, static_text =
pcall(self.fn, args, self.parent, unpack(self.user_args))
if not ok then
print(
update_errorstring:format(
self.indx,
self.parent.snippet.name,
static_text
)
)
static_text = { "" }
end
self.static_text =
util.indent(util.to_string_table(static_text), self.parent.indentstr)
end
function FunctionNode:update_restore()
-- only if args still match.
if self.static_text and vim.deep_equal(self:get_args(), self.last_args) then
self:set_text(self.static_text)
else
self:update()
end
end
-- FunctionNode's don't have static text, only set visibility.
function FunctionNode:put_initial(_)
self.visible = true
end
function FunctionNode:indent(_) end
function FunctionNode:expand_tabs(_) end
function FunctionNode:make_args_absolute(position_so_far)
self.args_absolute = {}
node_util.make_args_absolute(self.args, position_so_far, self.args_absolute)
end
function FunctionNode:set_dependents()
local dict = self.parent.snippet.dependents_dict
local append_list =
vim.list_extend({ "dependents" }, self.absolute_position)
append_list[#append_list + 1] = "dependent"
for _, arg in ipairs(self.args_absolute) do
-- if arg is a luasnip-node, just insert it as the key.
-- important!! rawget, because indexing absolute_indexer with some key
-- appends the key.
-- Maybe this is stupid??
if rawget(arg, "type") ~= nil then
dict:set(vim.list_extend({ arg }, append_list), self)
elseif arg.absolute_insert_position then
-- copy absolute_insert_position, list_extend mutates.
dict:set(
vim.list_extend(
vim.deepcopy(arg.absolute_insert_position),
append_list
),
self
)
elseif key_indexer.is_key(arg) then
dict:set(vim.list_extend({ "key", arg.key }, append_list), self)
end
end
end
function FunctionNode:is_interactive()
-- the function node is only evaluated once if it has no argnodes -> it's
-- not interactive then.
return #self.args ~= 0
end
return {
F = F,
FunctionNode = FunctionNode,
}

View File

@ -0,0 +1,330 @@
local Node = require("luasnip.nodes.node")
local InsertNode = Node.Node:new()
local ExitNode = InsertNode:new()
local util = require("luasnip.util.util")
local node_util = require("luasnip.nodes.util")
local types = require("luasnip.util.types")
local events = require("luasnip.util.events")
local extend_decorator = require("luasnip.util.extend_decorator")
local function I(pos, static_text, opts)
static_text = util.to_string_table(static_text)
if pos == 0 then
return ExitNode:new({
pos = pos,
static_text = static_text,
mark = nil,
dependents = {},
type = types.exitNode,
-- will only be needed for 0-node, -1-node isn't set with this.
ext_gravities_active = { false, false },
}, opts)
else
return InsertNode:new({
pos = pos,
static_text = static_text,
mark = nil,
dependents = {},
type = types.insertNode,
inner_active = false,
}, opts)
end
end
extend_decorator.register(I, { arg_indx = 3 })
function ExitNode:input_enter(no_move, dry_run)
if dry_run then
return
end
-- Don't enter node for -1-node, it isn't in the node-table.
if self.pos == 0 then
InsertNode.input_enter(self, no_move, dry_run)
else
-- -1-node:
-- set rgrav true on left side of snippet. Text inserted now pushes the
-- snippet, and is not contained in it.
local begin_pos = self.mark:pos_begin_raw()
self.parent:subtree_set_pos_rgrav(begin_pos, 1, true)
if not no_move then
if vim.fn.mode() == "i" then
util.insert_move_on(begin_pos)
else
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
"n",
true
)
util.normal_move_on_insert(begin_pos)
end
end
self:event(events.enter)
end
end
function ExitNode:focus()
local lrgrav, rrgrav
local snippet = self.parent
-- if last of first node of the snippet, make inserted text move out of snippet.
if snippet.nodes[#snippet.nodes] == self then
lrgrav = false
rrgrav = false
elseif snippet.nodes[1] == self then
lrgrav = true
rrgrav = true
else
lrgrav = false
rrgrav = true
end
Node.focus_node(self, lrgrav, rrgrav)
end
function ExitNode:input_leave(no_move, dry_run)
if dry_run then
return
end
if self.pos == 0 then
InsertNode.input_leave(self, no_move, dry_run)
else
self:event(events.leave)
end
end
function ExitNode:_update_dependents() end
function ExitNode:update_dependents() end
function ExitNode:update_all_dependents() end
function ExitNode:_update_dependents_static() end
function ExitNode:update_dependents_static() end
function ExitNode:update_all_dependents_static() end
function ExitNode:is_interactive()
return true
end
function InsertNode:input_enter(no_move, dry_run)
if dry_run then
return
end
self.visited = true
self.mark:update_opts(self.ext_opts.active)
-- no_move only prevents moving the cursor, but the active node should
-- still be focused.
self:focus()
if not no_move then
-- SELECT snippet text only when there is text to select (more oft than not there isnt).
local mark_begin_pos, mark_end_pos = self.mark:pos_begin_end_raw()
if not util.pos_equal(mark_begin_pos, mark_end_pos) then
util.any_select(mark_begin_pos, mark_end_pos)
else
-- if current and target mode is INSERT, there's no reason to leave it.
if vim.fn.mode() == "i" then
util.insert_move_on(mark_begin_pos)
else
-- mode might be VISUAL or something else, but <Esc> always leads to normal.
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
"n",
true
)
util.normal_move_on_insert(mark_begin_pos)
end
end
end
self:event(events.enter)
end
-- only necessary for insertNodes, inner_active (unlike `active`) does not occur
-- in other nodes.
-- Since insertNodes don't have `active`, we can use the dry_run.active-field
-- for this.
function InsertNode:init_dry_run_inner_active(dry_run)
if dry_run and dry_run.active[self] == nil then
dry_run.active[self] = self.inner_active
end
end
function InsertNode:is_inner_active(dry_run)
return (not dry_run and self.inner_active)
or (dry_run and dry_run.active[self])
end
function InsertNode:jump_into(dir, no_move, dry_run)
self:init_dry_run_inner_active(dry_run)
if self:is_inner_active(dry_run) then
if dir == 1 then
if self.next then
self:input_leave_children(dry_run)
self:input_leave(no_move, dry_run)
return self.next:jump_into(dir, no_move, dry_run)
else
return nil
end
else
if self.prev then
self:input_leave_children(dry_run)
self:input_leave(no_move, dry_run)
return self.prev:jump_into(dir, no_move, dry_run)
else
return nil
end
end
else
self:input_enter(no_move, dry_run)
return self
end
end
function ExitNode:jump_from(dir, no_move, dry_run)
self:init_dry_run_inner_active(dry_run)
local next_node = util.ternary(dir == 1, self.next, self.prev)
local next_inner_node =
util.ternary(dir == 1, self.inner_first, self.inner_last)
if next_inner_node then
self:input_enter_children(dry_run)
return next_inner_node:jump_into(dir, no_move, dry_run)
else
if next_node then
local next_node_dry_run = { active = {} }
-- don't have to `init_dry_run_inner_active` since this node does
-- not have children active if jump_from is called.
-- true: don't move
local target_node =
next_node:jump_into(dir, true, next_node_dry_run)
-- if there is no node that can serve as jump-target, just remain
-- here.
-- Regular insertNodes don't have to handle this, since there is
-- always an exitNode or another insertNode at their endpoints.
if not target_node then
return self
end
self:input_leave(no_move, dry_run)
return next_node:jump_into(dir, no_move, dry_run) or self
else
return self
end
end
end
function InsertNode:jump_from(dir, no_move, dry_run)
self:init_dry_run_inner_active(dry_run)
local next_node = util.ternary(dir == 1, self.next, self.prev)
local next_inner_node =
util.ternary(dir == 1, self.inner_first, self.inner_last)
if next_inner_node then
self:input_enter_children(dry_run)
return next_inner_node:jump_into(dir, no_move, dry_run)
else
if next_node then
self:input_leave(no_move, dry_run)
return next_node:jump_into(dir, no_move, dry_run)
end
end
end
function InsertNode:input_enter_children(dry_run)
if dry_run then
dry_run.active[self] = true
else
self.inner_active = true
end
end
function InsertNode:input_leave_children(dry_run)
if dry_run then
dry_run.active[self] = false
else
self.inner_active = false
end
end
function InsertNode:input_leave(_, dry_run)
if dry_run then
return
end
self:event(events.leave)
self:update_dependents()
self.mark:update_opts(self:get_passive_ext_opts())
end
function InsertNode:exit()
if self.inner_first then
self.inner_first:exit()
end
self.visible = false
self.inner_first = nil
self.inner_last = nil
self.inner_active = false
self.mark:clear()
end
function InsertNode:get_docstring()
-- copy as to not in-place-modify static text.
return util.string_wrap(self.static_text, rawget(self, "pos"))
end
function InsertNode:is_interactive()
return true
end
function InsertNode:child_snippets()
local own_child_snippets = {}
for _, child_snippet in ipairs(self.parent.snippet.child_snippets) do
if child_snippet.parent_node == self then
table.insert(own_child_snippets, child_snippet)
end
end
return own_child_snippets
end
function InsertNode:subtree_set_pos_rgrav(pos, direction, rgrav)
self.mark:set_rgrav(-direction, rgrav)
local own_child_snippets = self:child_snippets()
local child_from_indx
if direction == 1 then
child_from_indx = 1
else
child_from_indx = #own_child_snippets
end
node_util.nodelist_adjust_rgravs(
own_child_snippets,
child_from_indx,
pos,
direction,
rgrav,
-- don't assume that the child-snippets are all adjacent.
false
)
end
function InsertNode:subtree_set_rgrav(rgrav)
self.mark:set_rgravs(rgrav, rgrav)
local own_child_snippets = self:child_snippets()
for _, child_snippet in ipairs(own_child_snippets) do
child_snippet:subtree_set_rgrav(rgrav)
end
end
return {
I = I,
}

View File

@ -0,0 +1,12 @@
local M = {}
local key_mt = {}
function M.new_key(key)
return setmetatable({ key = key }, key_mt)
end
function M.is_key(t)
return getmetatable(t) == key_mt
end
return M

View File

@ -0,0 +1,117 @@
local snip_mod = require("luasnip.nodes.snippet")
local node_util = require("luasnip.nodes.util")
local extend_decorator = require("luasnip.util.extend_decorator")
local VirtualSnippet = {}
local VirtualSnippet_mt = { __index = VirtualSnippet }
function VirtualSnippet:get_docstring()
return self.snippet:get_docstring()
end
function VirtualSnippet:copy()
local copy = self.snippet:copy()
copy.id = self.id
return copy
end
-- VirtualSnippet has all the fields for executing these methods.
VirtualSnippet.matches = snip_mod.Snippet.matches
VirtualSnippet.invalidate = snip_mod.Snippet.invalidate
---Create new virtual snippet, ie. an object which is capable of performning
---all the functions expected from a snippet which is yet to be expanded
---(`matches`,`get_docstring`,`invalidate`,`retrieve_all`,`copy`)
---@param context context as defined for snippet-constructor. Table, not nil.
---@param snippet The snippet this virtual snippet will return on `copy`, also not nil.
---@param opts opts as defined for snippet-constructor. Has to be a table, may be empty.
local function new_virtual_snippet(context, snippet, opts)
-- init fields necessary for matches, invalidate, adding the snippet.
local o = snip_mod.init_snippet_context(context, opts)
o.snippet = snippet
setmetatable(o, VirtualSnippet_mt)
return o
end
local MultiSnippet = {}
local MultiSnippet_mt = { __index = MultiSnippet }
function MultiSnippet:retrieve_all()
return self.v_snips
end
local function multisnippet_from_snippet_obj(contexts, snippet, snippet_opts)
assert(
type(contexts) == "table",
"multisnippet: expected contexts to be a table."
)
local common_context = node_util.wrap_context(contexts.common) or {}
local v_snips = {}
for _, context in ipairs(contexts) do
local complete_context = vim.tbl_extend(
"keep",
node_util.wrap_context(context),
common_context
)
table.insert(
v_snips,
new_virtual_snippet(complete_context, snippet, snippet_opts)
)
end
local o = {
v_snips = v_snips,
}
setmetatable(o, MultiSnippet_mt)
return o
end
local function multisnippet_from_nodes(contexts, nodes, opts)
opts = opts or {}
local common_snip_opts = opts.common_opts or {}
-- create snippet without `context`-fields!
-- compare to `S` (aka `s`, the default snippet-constructor) in
-- `nodes/snippet.lua`.
return multisnippet_from_snippet_obj(
contexts,
snip_mod._S(
snip_mod.init_snippet_opts(common_snip_opts),
nodes,
common_snip_opts
),
common_snip_opts
)
end
local function extend_multisnippet_contexts(passed_arg, extend_arg)
-- extend passed arg with contexts passed in extend-call
vim.list_extend(passed_arg, extend_arg)
-- extend ("keep") valid keyword-arguments.
passed_arg.common = vim.tbl_deep_extend(
"keep",
node_util.wrap_context(passed_arg.common) or {},
node_util.wrap_context(extend_arg.common) or {}
)
return passed_arg
end
extend_decorator.register(
multisnippet_from_nodes,
-- first arg needs special handling (extend list of contexts (index i
-- becomes i+#passed_arg, not i again))
{ arg_indx = 1, extend = extend_multisnippet_contexts },
-- opts can just be `vim.tbl_extend`ed.
{ arg_indx = 3 }
)
return {
new_multisnippet = multisnippet_from_nodes,
_raw_ms = multisnippet_from_snippet_obj,
}

View File

@ -0,0 +1,643 @@
local session = require("luasnip.session")
local util = require("luasnip.util.util")
local node_util = require("luasnip.nodes.util")
local ext_util = require("luasnip.util.ext_opts")
local events = require("luasnip.util.events")
local key_indexer = require("luasnip.nodes.key_indexer")
local types = require("luasnip.util.types")
local Node = {}
function Node:new(o, opts)
o = o or {}
-- visible is true if the node is visible on-screen, during normal
-- expansion, static_visible is needed for eg. get_static_text, where
-- argnodes in inactive choices will happily provide their static text,
-- which leads to inaccurate docstrings.
o.visible = false
o.static_visible = false
o.old_text = {}
o.visited = false
-- override existing keys, might be necessary due to double-init from
-- snippetProxy, but shouldn't hurt.
o = vim.tbl_extend("force", o, node_util.init_node_opts(opts or {}))
setmetatable(o, self)
self.__index = self
return o
end
function Node:get_static_text()
-- return nil if not visible.
-- This will prevent updates if not all nodes are visible during
-- docstring/static_text-generation. (One example that would otherwise fail
-- is the following snippet:
--
-- s("trig", {
-- i(1, "cccc"),
-- t" ",
-- c(2, {
-- t"aaaa",
-- i(nil, "bbbb")
-- }),
-- f(function(args) return args[1][1]..args[2][1] end, {ai[2][2], 1} )
-- })
--
-- )
-- By also allowing visible, and not only static_visible, the docstrings
-- generated during `get_current_choices` (ie. without having the whole
-- snippet `static_init`ed) get better.
if not self.visible and not self.static_visible then
return nil
end
return self.static_text
end
function Node:get_docstring()
-- visibility only matters for get_static_text because that's called for
-- argnodes whereas get_docstring will only be called for actually
-- visible nodes.
return self.static_text
end
function Node:put_initial(pos)
-- access static text directly, get_static_text() won't work due to
-- static_visible not being set.
util.put(self.static_text, pos)
self.visible = true
end
function Node:input_enter(_, _)
self.visited = true
self.mark:update_opts(self.ext_opts.active)
self:event(events.enter)
end
-- dry_run: if not nil, it has to be a table with the key `active` also a table.
-- dry_run.active[node] stores whether the node is "active" in the dry run (we
-- can't change the `active`-state in the actual node, so changes to the
-- active-state are stored in the `dry_run`-table, which is passed to all nodes
-- that participate in the jump)
-- The changes to `active` have to be stored. Otherwise, `dry_run` can lead to
-- endless loops in cases like:
-- ```lua
-- s({ trig = 'n' } , { i(1, "1"), sn(2, {t"asdf"}), i(3, "3") })
-- ```
--
-- Here, jumping from 1 will first set active on the snippetNode, then, since
-- there are no interactive nodes inside it, and since active is set, we will
-- jump to the `i(3)`.
-- If active is not set during the dry_run, we will just keep jumping into the
-- inner textNode.
--
-- A similar problem occurs in nested expansions (insertNode.inner_active
-- is not set).
function Node:jump_into(_, no_move, dry_run)
if not dry_run then
self:input_enter(no_move, dry_run)
end
return self
end
function Node:jump_from(dir, no_move, dry_run)
self:input_leave(no_move, dry_run)
if dir == 1 then
if self.next then
return self.next:jump_into(dir, no_move, dry_run)
else
return nil
end
else
if self.prev then
return self.prev:jump_into(dir, no_move, dry_run)
else
return nil
end
end
end
function Node:jumpable(dir)
if dir == 1 then
return self.next ~= nil
else
return self.prev ~= nil
end
end
function Node:get_text()
if not self.visible then
return nil
end
local ok, text = pcall(function()
local from_pos, to_pos = self.mark:pos_begin_end_raw()
-- end-exclusive indexing.
local lines =
vim.api.nvim_buf_get_lines(0, from_pos[1], to_pos[1] + 1, false)
if #lines == 1 then
lines[1] = string.sub(lines[1], from_pos[2] + 1, to_pos[2])
else
lines[1] = string.sub(lines[1], from_pos[2] + 1, #lines[1])
-- node-range is end-exclusive.
lines[#lines] = string.sub(lines[#lines], 1, to_pos[2])
end
return lines
end)
-- if deleted.
return ok and text or { "" }
end
function Node:set_old_text()
self.old_text = self:get_text()
end
function Node:exit()
self.visible = false
self.mark:clear()
end
function Node:get_passive_ext_opts()
if self.visited then
return self.ext_opts.visited
else
return self.ext_opts.unvisited
end
end
function Node:input_leave(_, dry_run)
if dry_run then
return
end
self:event(events.leave)
self.mark:update_opts(self:get_passive_ext_opts())
end
function Node:input_leave_children() end
function Node:input_enter_children() end
local function find_dependents(self, position_self, dict)
local nodes = {}
-- this might also be called from a node which does not possess a position!
-- (for example, a functionNode may be depended upon via its key)
if position_self then
position_self[#position_self + 1] = "dependents"
vim.list_extend(nodes, dict:find_all(position_self, "dependent") or {})
position_self[#position_self] = nil
end
vim.list_extend(
nodes,
dict:find_all({ self, "dependents" }, "dependent") or {}
)
if self.key then
vim.list_extend(
nodes,
dict:find_all({ "key", self.key, "dependents" }, "dependent") or {}
)
end
return nodes
end
function Node:_update_dependents()
local dependent_nodes = find_dependents(
self,
self.absolute_insert_position,
self.parent.snippet.dependents_dict
)
if #dependent_nodes == 0 then
return
end
for _, node in ipairs(dependent_nodes) do
if node.visible then
node:update()
end
end
end
-- _update_dependents is the function to update the nodes' dependents,
-- update_dependents is what will actually be called.
-- This allows overriding update_dependents in a parent-node (eg. snippetNode)
-- while still having access to the original function (for subsequent overrides).
Node.update_dependents = Node._update_dependents
-- update_all_dependents is used to update all nodes' dependents in a
-- snippet-tree. Necessary in eg. set_choice (especially since nodes may have
-- dependencies outside the tree itself, so update_all_dependents should take
-- care of those too.)
Node.update_all_dependents = Node._update_dependents
function Node:_update_dependents_static()
local dependent_nodes = find_dependents(
self,
self.absolute_insert_position,
self.parent.snippet.dependents_dict
)
if #dependent_nodes == 0 then
return
end
for _, node in ipairs(dependent_nodes) do
if node.static_visible then
node:update_static()
end
end
end
Node.update_dependents_static = Node._update_dependents_static
Node.update_all_dependents_static = Node._update_dependents_static
function Node:update() end
function Node:update_static() end
function Node:expand_tabs(tabwidth, indentstr)
util.expand_tabs(self.static_text, tabwidth, indentstr)
end
function Node:indent(indentstr)
util.indent(self.static_text, indentstr)
end
function Node:subsnip_init() end
function Node:init_positions(position_so_far)
self.absolute_position = vim.deepcopy(position_so_far)
end
function Node:init_insert_positions(position_so_far)
self.absolute_insert_position = vim.deepcopy(position_so_far)
end
function Node:event(event)
local node_callback = self.node_callbacks[event]
if node_callback then
node_callback(self)
end
-- try to get the callback from the parent.
if self.pos then
-- node needs position to get callback (nodes may not have position if
-- defined in a choiceNode, ie. c(1, {
-- i(nil, {"works!"})
-- }))
-- works just fine.
local parent_callback = self.parent.callbacks[self.pos][event]
if parent_callback then
parent_callback(self)
end
end
session.event_node = self
vim.api.nvim_exec_autocmds("User", {
pattern = "Luasnip" .. events.to_string(self.type, event),
modeline = false,
})
end
local function get_args(node, get_text_func_name)
local argnodes_text = {}
for _, arg in ipairs(node.args_absolute) do
local argnode
if key_indexer.is_key(arg) then
argnode = node.parent.snippet.dependents_dict:get({
"key",
arg.key,
"node",
})
else
-- since arg may be a node, it may not be initialized in the snippet
-- and therefore not have an absolute_insert_position. Check for that.
if not arg.absolute_insert_position then
-- the node is not (yet, maybe) visible.
return nil
end
local dict_key = arg.absolute_insert_position
-- will append to arg.absolute_insert_position, but it's restored
-- two lines down.
-- (dict:get shouldn't (yeah yeah, you never know, but this really
-- shouldn't) fail, so we don't worry with pcall)
table.insert(dict_key, "node")
argnode = node.parent.snippet.dependents_dict:get(dict_key)
dict_key[#dict_key] = nil
end
-- maybe the node is part of a dynamicNode and not yet generated.
if not argnode then
return nil
end
local argnode_text = argnode[get_text_func_name](argnode)
-- can only occur with `get_text`. If one returns nil, the argnode
-- isn't visible or some other error occured. Either way, return nil
-- to signify that not all argnodes are available.
if not argnode_text then
return nil
end
table.insert(argnodes_text, argnode_text)
end
return argnodes_text
end
function Node:get_args()
return get_args(self, "get_text")
end
function Node:get_static_args()
return get_args(self, "get_static_text")
end
function Node:get_jump_index()
return self.pos
end
function Node:set_ext_opts(name)
-- differentiate, either visited or unvisited needs to be set.
if name == "passive" then
self.mark:update_opts(self:get_passive_ext_opts())
else
self.mark:update_opts(self.ext_opts[name])
end
end
-- for insert,functionNode.
function Node:store()
self.static_text = self:get_text()
end
function Node:update_restore() end
-- find_node only needs to check children, self is checked by the parent.
function Node:find_node()
return nil
end
Node.ext_gravities_active = { false, true }
function Node:insert_to_node_absolute(position)
-- this node is a leaf, just return its position
return self.absolute_position
end
function Node:set_dependents() end
function Node:set_argnodes(dict)
if self.absolute_insert_position then
-- append+remove "node" from absolute_insert_position to quickly create
-- key for dict.
table.insert(self.absolute_insert_position, "node")
dict:set(self.absolute_insert_position, self)
self.absolute_insert_position[#self.absolute_insert_position] = nil
end
if self.key then
dict:set({ "key", self.key, "node" }, self)
end
end
function Node:make_args_absolute() end
function Node:resolve_position(position)
error(
string.format(
"invalid resolve_position(%d) on node at %s",
position,
vim.inspect(self.absolute_position)
)
)
end
function Node:static_init()
self.static_visible = true
end
-- resolve_*node*_ext_opts because snippet(Node)s have child_ext_opts, which
-- also have to be resolved.
-- This function generates a nodes ext_opts (those actually used in highlighting).
function Node:resolve_node_ext_opts(base_prio, parent_ext_opts)
if self.merge_node_ext_opts then
self.ext_opts = ext_util.extend(
vim.deepcopy(self.node_ext_opts),
parent_ext_opts or self.parent.effective_child_ext_opts[self.type]
)
else
self.ext_opts = self.node_ext_opts
end
ext_util.set_abs_prio(
self.ext_opts,
(base_prio or self.parent.ext_opts.base_prio)
+ session.config.ext_prio_increase
)
end
function Node:is_interactive()
-- safe default.
return true
end
-- initialize active-setting in dry_run-table for `self`.
function Node:init_dry_run_active(dry_run)
if dry_run and dry_run.active[self] == nil then
dry_run.active[self] = self.active
end
end
-- determine whether this node is currently active.
-- This is its own function (and not just a flat table-check) since we have to
-- check the data in the dry_run-table or the node, depending on `dry_run`.
function Node:is_active(dry_run)
return (not dry_run and self.active) or (dry_run and dry_run.active[self])
end
function Node:get_buf_position(opts)
opts = opts or {}
local raw = opts.raw ~= nil and opts.raw or true
if raw then
return self.mark:pos_begin_end_raw()
else
return self.mark:pos_begin_end()
end
end
-- only does something for insert- and snippetNode.
function Node:set_sibling_rgravs(_, _, _, _) end
-- when an insertNode receives text, its mark/region should contain all the
-- text that is inserted.
-- This can be achieved by setting the left and right "right-gravity"(rgrav) of
-- the mark, which are responsible for controlling the direction an endpoint of
-- the mark is moved when text is inserted.
-- When a regular insertNode is focused/entered, we would like the left and
-- right rgrav to be false and true, respectively. Example:
-- this is an insertNodeAnd this is another insertNode
-- mark1: l r
-- mark2: l r
-- if `this is an insertNode` should be focused, we have to set the rgrav of
-- l1 false (because inserting text at the column of l1 should not shift l1 to
-- the right). Similarly, the rgrav of r1 has to be set true, text inserted at
-- its column SHOULD move it to the right.
-- Complicating this whole thing: if like above there is an adjacent
-- insertNode, its gravities have to be adjusted as well (if they are not, the
-- insertNodes regions would overlap, which is obviously confusing). So, when
-- adjusting some nodes rgravs, those of the siblings may have to be adjusted as well.
-- Another example:
-- aacc
-- mark1: l r
-- mark2: l
-- r
-- mark3: l r
-- (the insertNode for mark2 is not visible at all, l2 and r2 are in the same
-- column)
-- This example highlights that not only the immediate sibling might need
-- adjusting, but all siblings that share a mark-boundary with the node that
-- should be focused.
-- Even further complicating the matter: Snippets are trees, and failing to
-- set the rgrav of snippet adjacent to (sharing an endpoint with) the node we
-- want to focus, regardless of its position in the tree, will lead to extmarks
-- covering the wrong regions.
--
-- More complications: focusing a node does not always mean setting the rgravs
-- such that text will end up inside the node!
-- For example, in the case of a terminating i(0) (like s("trig", {i(1,
-- "text"), t" ", i(0)})), we would like to NOT include the text entered into
-- it in the snippet. Thus, the gravities of it and all its parents have to be
-- set (in this case) false,false, if the i(0) were at the beginning of the
-- snippet (weird??) they'd have to be true,true.
--
--
-- Unfortunately, we cannot guarantee that two extmarks on the same position
-- also have the same gravities, for exmample if the text inside a focused node
-- is deleted, and then another unrelated node is focused, the two endpoints of
-- the previously focused node will have opposing rgravs.
-- Maybe this whole procedure could be sped up further if we can assume that
-- identical endpoints imply identical rgravs.
local function focus_node(self, lrgrav, rrgrav)
-- find nodes on path from self to root.
local nodes_path = node_util.root_path(self)
-- direction is the direction away from this node, towards the outside of
-- the tree-representation of the snippet.
-- This is dubbed "direction" because it is the direction we will search in
-- to find nodes on one endpoint of self.
for _, direction in ipairs({ -1, 1 }) do
local self_direction_endpoint = self.mark:get_endpoint(direction)
local direction_rgrav = util.ternary(direction == -1, lrgrav, rrgrav)
local effective_direction_rgrav = direction_rgrav
-- adjust left rgrav of all nodes on path upwards to root/snippet:
-- (i st. self and the snippet are both handled)
for i = 1, #nodes_path do
local node = nodes_path[i]
local node_direction_endpoint = node.mark:get_endpoint(direction)
if
not util.pos_equal(
node_direction_endpoint,
self_direction_endpoint
)
then
-- stop adjusting rgravs once self no longer is on the boundary of
-- its parents, or if the rgrav is already set correctly.
break
end
node.mark:set_rgrav(direction, effective_direction_rgrav)
-- Once self's snippet is reached on the root-path, we will only
-- adjust nodes self should be completely contained inside.
-- Since the rgravs, however, may be set up otherwise (for example
-- when focusing on an $0 that is the last node of the snippet), we
-- have to adjust them now.
if node.snippet == node then
effective_direction_rgrav = direction == 1
end
-- can't use node.parent, since that might skip nodes (in the case of
-- dynamicNode, for example, the generated snippets parent is not the
-- dynamicNode, but its parent).
-- also: don't need to check for nil, because the
local node_above = nodes_path[i + 1]
if node_above then
node_above:set_sibling_rgravs(
node,
self_direction_endpoint,
direction,
effective_direction_rgrav
)
end
end
self:subtree_set_pos_rgrav(
self_direction_endpoint,
-direction,
direction_rgrav
)
end
end
function Node:subtree_set_rgrav(rgrav)
self.mark:set_rgravs(rgrav, rgrav)
end
function Node:subtree_set_pos_rgrav(_, direction, rgrav)
self.mark:set_rgrav(-direction, rgrav)
end
function Node:focus()
focus_node(self, false, true)
end
function Node:set_text(text)
self:focus()
local node_from, node_to = self.mark:pos_begin_end_raw()
local ok = pcall(
vim.api.nvim_buf_set_text,
0,
node_from[1],
node_from[2],
node_to[1],
node_to[2],
text
)
-- we can assume that (part of) the snippet was deleted; remove it from
-- the jumplist.
if not ok then
error("[LuaSnip Failed]: " .. vim.inspect(text))
end
end
-- since parents validate the adjacency, nodes where we don't know anything
-- about the text inside them just have to assume they haven't been deleted :D
function Node:extmarks_valid()
return true
end
function Node:linkable()
-- linkable if insert or exitNode.
return vim.tbl_contains(
{ types.insertNode, types.exitNode },
rawget(self, "type")
)
end
function Node:interactive()
-- interactive if immediately inside choiceNode.
return vim.tbl_contains(
{ types.insertNode, types.exitNode },
rawget(self, "type")
) or rawget(self, "choice") ~= nil
end
function Node:leaf()
return vim.tbl_contains(
{ types.textNode, types.functionNode, types.insertNode, types.exitNode },
rawget(self, "type")
)
end
return {
Node = Node,
focus_node = focus_node,
}

View File

@ -0,0 +1,307 @@
-- restoreNode is implemented similarly to dynamicNode, only that it gets the snippetNode not from some function, but from self.snip.stored[key].
local Node = require("luasnip.nodes.node").Node
local wrap_nodes_in_snippetNode =
require("luasnip.nodes.snippet").wrap_nodes_in_snippetNode
local RestoreNode = Node:new()
local types = require("luasnip.util.types")
local events = require("luasnip.util.events")
local util = require("luasnip.util.util")
local node_util = require("luasnip.nodes.util")
local mark = require("luasnip.util.mark").mark
local extend_decorator = require("luasnip.util.extend_decorator")
local function R(pos, key, nodes, opts)
-- don't create nested snippetNodes, unnecessary.
nodes = nodes and wrap_nodes_in_snippetNode(nodes)
return RestoreNode:new({
pos = pos,
key = key,
mark = nil,
snip = nodes,
type = types.restoreNode,
dependents = {},
-- TODO: find out why it's necessary only for this node.
active = false,
}, opts)
end
extend_decorator.register(R, { arg_indx = 4 })
function RestoreNode:exit()
if not self.visible then
-- already exited.
return
end
self.visible = false
self.mark:clear()
-- snip should exist if exit is called.
self.snip:store()
-- will be copied on restore, no need to copy here too.
self.parent.snippet.stored[self.key] = self.snip
self.snip:exit()
self.snip = nil
self.active = false
end
function RestoreNode:input_enter(_, dry_run)
if dry_run then
dry_run.active[self] = true
return
end
self.active = true
self.visited = true
self.mark:update_opts(self.ext_opts.active)
self:event(events.enter)
end
function RestoreNode:input_leave(_, dry_run)
if dry_run then
dry_run.active[self] = false
return
end
self:event(events.leave)
self:update_dependents()
self.active = false
self.mark:update_opts(self:get_passive_ext_opts())
end
-- set snippetNode for this key here.
function RestoreNode:subsnip_init()
-- don't overwrite potentially stored snippetNode.
-- due to metatable, there will always be a node set, but only those set
-- by it (should) have the is_default set to true.
if self.parent.snippet.stored[self.key].is_default and self.snip then
self.parent.snippet.stored[self.key] = self.snip
end
end
-- don't need these, will be done in put_initial and get_static/docstring.
function RestoreNode:indent(_) end
function RestoreNode:expand_tabs(_) end
-- will be called when before expansion but after snip.parent was initialized.
-- Get the actual snippetNode here.
function RestoreNode:put_initial(pos)
local tmp = self.parent.snippet.stored[self.key]
-- act as if snip is directly inside parent.
tmp.parent = self.parent
tmp.indx = self.indx
tmp.next = self
tmp.prev = self
tmp.snippet = self.parent.snippet
tmp.restore_node = self
tmp.update_dependents = function(node)
node:_update_dependents()
-- self is restoreNode.
node.restore_node:update_dependents()
end
tmp:resolve_child_ext_opts()
tmp:resolve_node_ext_opts()
tmp:subsnip_init()
tmp:init_positions(self.snip_absolute_position)
tmp:init_insert_positions(self.snip_absolute_insert_position)
tmp:make_args_absolute()
tmp:set_dependents()
tmp:set_argnodes(self.parent.snippet.dependents_dict)
if vim.bo.expandtab then
tmp:expand_tabs(util.tab_width(), self.parent.indentstring)
end
-- correctly set extmark for node.
-- does not modify ext_opts[node.type].
local mark_opts = vim.tbl_extend("keep", {
right_gravity = false,
end_right_gravity = false,
}, tmp:get_passive_ext_opts())
local old_pos = vim.deepcopy(pos)
tmp:put_initial(pos)
tmp.mark = mark(old_pos, pos, mark_opts)
-- no need to call update here, will be done by function calling this
-- function.
self.snip = tmp
self.visible = true
end
-- the same as DynamicNode.
function RestoreNode:jump_into(dir, no_move, dry_run)
self:init_dry_run_active(dry_run)
if self:is_active(dry_run) then
self:input_leave(no_move, dry_run)
if dir == 1 then
return self.next:jump_into(dir, no_move, dry_run)
else
return self.prev:jump_into(dir, no_move, dry_run)
end
else
self:input_enter(no_move, dry_run)
return self.snip:jump_into(dir, no_move, dry_run)
end
end
function RestoreNode:set_ext_opts(name)
Node.set_ext_opts(self, name)
self.snip:set_ext_opts(name)
end
function RestoreNode:update()
self.snip:update()
end
function RestoreNode:update_static()
-- *_static-methods can use the stored snippet, since they don't require
-- the snip to actually be inside the restoreNode.
self.parent.snippet.stored[self.key]:update_static()
end
local function snip_init(self, snip)
snip.parent = self.parent
snip.snippet = self.parent.snippet
-- pos should be nil if the restoreNode is inside a choiceNode.
snip.pos = rawget(self, "pos")
snip:resolve_child_ext_opts()
snip:resolve_node_ext_opts()
snip:subsnip_init()
snip:init_positions(self.snip_absolute_position)
snip:init_insert_positions(self.snip_absolute_insert_position)
snip:make_args_absolute()
snip:set_dependents()
snip:set_argnodes(self.parent.snippet.dependents_dict)
snip:static_init()
end
function RestoreNode:static_init()
Node.static_init(self)
self.snip = self.parent.snippet.stored[self.key]
snip_init(self, self.snip)
end
function RestoreNode:get_static_text()
-- cache static_text, no need to recalculate function.
if not self.static_text then
self.static_text =
self.parent.snippet.stored[self.key]:get_static_text()
end
return self.static_text
end
function RestoreNode:get_docstring()
if not self.docstring then
self.docstring = self.parent.snippet.stored[self.key]:get_docstring()
end
return self.docstring
end
function RestoreNode:store() end
-- will be restored through other means.
function RestoreNode:update_restore()
self.snip:update_restore()
end
function RestoreNode:find_node(predicate)
if self.snip then
if predicate(self.snip) then
return self.snip
else
return self.snip:find_node(predicate)
end
end
return nil
end
function RestoreNode:insert_to_node_absolute(position)
if #position == 0 then
return self.absolute_position
end
-- nil if not yet available.
return self.snip and self.snip:insert_to_node_absolute(position)
end
function RestoreNode:update_all_dependents()
self:_update_dependents()
self.snip:update_all_dependents()
end
function RestoreNode:update_all_dependents_static()
self:_update_dependents_static()
self.parent.snippet.stored[self.key]:_update_dependents_static()
end
function RestoreNode:init_insert_positions(position_so_far)
Node.init_insert_positions(self, position_so_far)
self.snip_absolute_insert_position =
vim.deepcopy(self.absolute_insert_position)
-- nodes of current snippet should have a 0 before.
self.snip_absolute_insert_position[#self.snip_absolute_insert_position + 1] =
0
end
function RestoreNode:init_positions(position_so_far)
Node.init_positions(self, position_so_far)
self.snip_absolute_position = vim.deepcopy(self.absolute_position)
-- Reach current snippet as snip_absolute_position..0.
self.snip_absolute_position[#self.snip_absolute_position + 1] = 0
end
function RestoreNode:resolve_position(position)
-- position must be 0, there are no other options.
return self.snip
end
function RestoreNode:is_interactive()
-- shouldn't be called, but revisit this once is_interactive is used in
-- places other than lsp-snippets.
return true
end
function RestoreNode:subtree_set_pos_rgrav(pos, direction, rgrav)
self.mark:set_rgrav(-direction, rgrav)
if self.snip then
self.snip:subtree_set_pos_rgrav(pos, direction, rgrav)
end
end
function RestoreNode:subtree_set_rgrav(rgrav)
self.mark:set_rgravs(rgrav, rgrav)
if self.snip then
self.snip:subtree_set_rgrav(rgrav)
end
end
function RestoreNode:extmarks_valid()
return node_util.generic_extmarks_valid(self, self.snip)
end
return {
R = R,
}

View File

@ -0,0 +1,125 @@
-- the idea of this class is to lazily parse snippet (eg. only on expansion).
--
-- This is achieved by returning a proxy that has enough information to tell
-- whether the snippet should be expanded at a given point (eg. all fields
-- necessary to perform Snippet:matches()), but doesn't actually
-- have to parse the snippet, leaving up-front cost of loading a bunch of
-- snippets at a minimum.
local lsp_parse_fn = require("luasnip.util.parser").parse_snippet
local snip_mod = require("luasnip.nodes.snippet")
local node_util = require("luasnip.nodes.util")
local extend_decorator = require("luasnip.util.extend_decorator")
local SnippetProxy = {}
-- add Snippet-functions SnippetProxy can perform using the available data.
SnippetProxy.matches = snip_mod.Snippet.matches
SnippetProxy.invalidate = snip_mod.Snippet.invalidate
SnippetProxy.retrieve_all = snip_mod.Snippet.retrieve_all
function SnippetProxy:get_docstring()
return self.docstring
end
function SnippetProxy:instantiate(parse_fn)
-- self already contains initialized context and opts, can just be passed
-- here, no problem.
-- Bonus: if some keys are set on the snippets in the table (from the
-- outside, for whatever reason), they are also present in the expanded
-- snippet.
--
-- _S will copy self, so we can safely mutate (set metatables).
local snippet = snip_mod._S(self, parse_fn(nil, self._snippet_string))
-- snippet will have snippetProxies `copy`, nil it in snippet so it calls
-- snippet-copy via metatable.
snippet.copy = nil
self._snippet = snippet
-- directly call into snippet on missing keys.
setmetatable(self, {
__index = self._snippet,
})
-- return snippet so it can provide a missing key.
return snippet
end
-- some values of the snippet are nil by default, list them here so snippets
-- aren't instantiated because of them.
local license_to_nil =
{ priority = true, snippetType = true, _source = true, filetype = true }
-- context and opts are (almost) the same objects as in s(contex, nodes, opts), snippet is a string representing the snippet.
-- opts can aditionally contain the key `parse_fn`, which will be used to parse
-- the snippet. This is useful, since snipmate-snippets are parsed with a
-- function than regular lsp-snippets.
-- context can be nil, in that case the resulting object can't be inserted into
-- the snippet-tables, but may be used after expansion (i.e. returned from
-- snippet:copy)
local function new(context, snippet, opts)
opts = opts or {}
-- default to regular lsp-parse-function.
local parse_fn = lsp_parse_fn
if opts.parse_fn then
parse_fn = opts.parse_fn
end
-- "error": there should not be duplicate keys, don't silently overwrite/keep.
local sp = vim.tbl_extend(
"error",
{},
context
and snip_mod.init_snippet_context(
node_util.wrap_context(context),
opts
)
or {},
snip_mod.init_snippet_opts(opts),
node_util.init_node_opts(opts)
)
sp._snippet_string = snippet
-- override docstring
sp.docstring = snippet
setmetatable(sp, {
__index = function(t, k)
if license_to_nil[k] then
-- k might be nil, return it.
return nil
end
if SnippetProxy[k] then
-- if it is possible to perform this operation without actually parsing the snippet, just do it.
return SnippetProxy[k]
end
local snip = SnippetProxy.instantiate(t, parse_fn)
if k == "_snippet" then
return snip
else
return snip[k]
end
end,
})
-- snippetProxy has to be able to return snippet on copy even after parsing,
-- when the metatable has been changed. Therefore: set copy in each instance
-- of snippetProxy.
function sp:copy()
local copy = self._snippet:copy()
copy.id = self.id
return copy
end
return sp
end
extend_decorator.register(
new,
{ arg_indx = 1, extend = node_util.snippet_extend_context },
{ arg_indx = 3 }
)
return new

View File

@ -0,0 +1,71 @@
local node_mod = require("luasnip.nodes.node")
local util = require("luasnip.util.util")
local types = require("luasnip.util.types")
local events = require("luasnip.util.events")
local extend_decorator = require("luasnip.util.extend_decorator")
local TextNode = node_mod.Node:new()
local function T(static_text, opts)
return TextNode:new({
static_text = util.to_string_table(static_text),
mark = nil,
type = types.textNode,
}, opts)
end
extend_decorator.register(T, { arg_indx = 2 })
function TextNode:input_enter(no_move, dry_run)
if dry_run then
return
end
self.mark:update_opts(self.ext_opts.active)
self.visited = true
if not no_move then
local mark_begin_pos = self.mark:pos_begin_raw()
if vim.fn.mode() == "i" then
util.insert_move_on(mark_begin_pos)
else
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
"n",
true
)
util.normal_move_on_insert(mark_begin_pos)
end
end
self:event(events.enter, no_move)
end
function TextNode:update_all_dependents() end
function TextNode:is_interactive()
-- a resounding false.
return false
end
function TextNode:extmarks_valid()
local from, to = self.mark:pos_begin_end_raw()
if
util.pos_cmp(from, to) == 0
and not (
#self.static_text == 0
or (#self.static_text == 1 and #self.static_text[1] == 0)
)
then
-- assume the snippet is invalid if a textNode occupies zero space,
-- but has text which would occupy some.
-- This should allow some modifications, but as soon as a textNode is
-- deleted entirely, we sound the alarm :D
return false
end
return true
end
return {
T = T,
textNode = TextNode,
}

View File

@ -0,0 +1,789 @@
local util = require("luasnip.util.util")
local ext_util = require("luasnip.util.ext_opts")
local types = require("luasnip.util.types")
local key_indexer = require("luasnip.nodes.key_indexer")
local session = require("luasnip.session")
local function subsnip_init_children(parent, children)
for _, child in ipairs(children) do
if child.type == types.snippetNode then
child.snippet = parent.snippet
child:resolve_child_ext_opts()
end
child:resolve_node_ext_opts()
child:subsnip_init()
end
end
local function init_child_positions_func(
key,
node_children_key,
child_func_name
)
-- maybe via load()?
return function(node, position_so_far)
node[key] = vim.deepcopy(position_so_far)
local pos_depth = #position_so_far + 1
for indx, child in ipairs(node[node_children_key]) do
position_so_far[pos_depth] = indx
child[child_func_name](child, position_so_far)
end
-- undo changes to position_so_far.
position_so_far[pos_depth] = nil
end
end
local function make_args_absolute(args, parent_insert_position, target)
for i, arg in ipairs(args) do
if type(arg) == "number" then
-- the arg is a number, should be interpreted relative to direct
-- parent.
local t = vim.deepcopy(parent_insert_position)
table.insert(t, arg)
target[i] = { absolute_insert_position = t }
else
-- insert node, absolute_indexer, or key itself, node's
-- absolute_insert_position may be nil, check for that during
-- usage.
target[i] = arg
end
end
end
local function wrap_args(args)
-- stylua: ignore
if type(args) ~= "table" or
(type(args) == "table" and args.absolute_insert_position) or
key_indexer.is_key(args) then
-- args is one single arg, wrap it.
return { args }
else
return args
end
end
-- includes child, does not include parent.
local function get_nodes_between(parent, child)
local nodes = {}
-- special case for nodes without absolute_position (which is only
-- start_node).
if child.pos == -1 then
-- no nodes between, only child.
nodes[1] = child
return nodes
end
local child_pos = child.absolute_position
local indx = #parent.absolute_position + 1
local prev = parent
while child_pos[indx] do
local next = prev:resolve_position(child_pos[indx])
nodes[#nodes + 1] = next
prev = next
indx = indx + 1
end
return nodes
end
-- assumes that children of child are not even active.
-- If they should also be left, do that separately.
-- Does not leave the parent.
local function leave_nodes_between(parent, child, no_move)
local nodes = get_nodes_between(parent, child)
if #nodes == 0 then
return
end
-- reverse order, leave child first.
for i = #nodes, 2, -1 do
-- this only happens for nodes where the parent will also be left
-- entirely (because we stop at nodes[2], and handle nodes[1]
-- separately)
nodes[i]:input_leave(no_move)
nodes[i - 1]:input_leave_children()
end
nodes[1]:input_leave(no_move)
end
local function enter_nodes_between(parent, child, no_move)
local nodes = get_nodes_between(parent, child)
if #nodes == 0 then
return
end
for i = 1, #nodes - 1 do
-- only enter children for nodes before the last (lowest) one.
nodes[i]:input_enter(no_move)
nodes[i]:input_enter_children()
end
nodes[#nodes]:input_enter(no_move)
end
local function select_node(node)
local node_begin, node_end = node.mark:pos_begin_end_raw()
util.any_select(node_begin, node_end)
end
local function print_dict(dict)
print(vim.inspect(dict, {
process = function(item, path)
if path[#path] == "node" or path[#path] == "dependent" then
return "node@" .. vim.inspect(item.absolute_position)
elseif path[#path] ~= vim.inspect.METATABLE then
return item
end
end,
}))
end
local function init_node_opts(opts)
local in_node = {}
if not opts then
opts = {}
end
-- copy once here, the opts might be reused.
in_node.node_ext_opts =
ext_util.complete(vim.deepcopy(opts.node_ext_opts or {}))
if opts.merge_node_ext_opts == nil then
in_node.merge_node_ext_opts = true
else
in_node.merge_node_ext_opts = opts.merge_node_ext_opts
end
in_node.key = opts.key
in_node.node_callbacks = opts.node_callbacks or {}
return in_node
end
local function snippet_extend_context(arg, extend)
if type(arg) == "string" then
arg = { trig = arg }
end
-- both are table or nil now.
return vim.tbl_extend("keep", arg or {}, extend or {})
end
local function wrap_context(context)
if type(context) == "string" then
return { trig = context }
else
return context
end
end
local function linkable_node(node)
-- node.type has to be one of insertNode, exitNode.
return vim.tbl_contains(
{ types.insertNode, types.exitNode },
rawget(node, "type")
)
end
-- mainly used internally, by binarysearch_pos.
-- these are the nodes that are definitely not linkable, there are nodes like
-- dynamicNode or snippetNode that might be linkable, depending on their
-- content. Could look into that to make this more complete, but that does not
-- feel appropriate (higher runtime), most cases should be served well by this
-- heuristic.
local function non_linkable_node(node)
return vim.tbl_contains(
{ types.textNode, types.functionNode },
rawget(node, "type")
)
end
-- return whether a node is certainly (not) interactive.
-- Coincindentially, the same nodes as (non-)linkable ones, but since there is a
-- semantic difference, use separate names.
local interactive_node = linkable_node
local non_interactive_node = non_linkable_node
local function prefer_nodes(prefer_func, reject_func)
return function(cmp_mid_to, cmp_mid_from, mid_node)
local reject_mid = reject_func(mid_node)
local prefer_mid = prefer_func(mid_node)
-- if we can choose which node to continue in, prefer the one that
-- may be linkable/interactive.
if cmp_mid_to == 0 and reject_mid then
return true, false
elseif cmp_mid_from == 0 and reject_mid then
return false, true
elseif (cmp_mid_to == 0 or cmp_mid_from == 0) and prefer_mid then
return false, false
else
return cmp_mid_to >= 0, cmp_mid_from < 0
end
end
end
-- functions for resolving conflicts, if `pos` is on the boundary of two nodes.
-- Return whether to continue behind or before mid (in that order).
-- At most one of those may be true, of course.
local binarysearch_preference = {
outside = function(cmp_mid_to, cmp_mid_from, _)
return cmp_mid_to >= 0, cmp_mid_from <= 0
end,
inside = function(cmp_mid_to, cmp_mid_from, _)
return cmp_mid_to > 0, cmp_mid_from < 0
end,
linkable = prefer_nodes(linkable_node, non_linkable_node),
interactive = prefer_nodes(interactive_node, non_interactive_node),
}
-- `nodes` is a list of nodes ordered by their occurrence in the buffer.
-- `pos` is a row-column-tuble, byte-columns, and we return the node the LEFT
-- EDGE(/side) of `pos` is inside.
-- This convention is chosen since a snippet inserted at `pos` will move the
-- character at `pos` to the right.
-- The exact meaning of "inside" can be influenced with `respect_rgravs` and
-- `boundary_resolve_mode`:
-- * if `respect_rgravs` is true, "inside" emulates the shifting-behaviour of
-- extmarks:
-- First of all, we compare the left edge of `pos` with the left/right edges
-- of from/to, depending on rgrav.
-- If the left edge is <= left/right edge of from, and < left/right edge of
-- to, `pos` is inside the node.
--
-- * if `respect_rgravs` is false, pos has to be fully inside a node to be
-- considered inside it. If pos is on the left endpoint, it is considered to be
-- left of the node, and likewise for the right endpoint.
--
-- * `boundary_resolve_mode` changes how a position on the boundary of a node
-- is treated:
-- * for `"prefer_linkable/interactive"`, we assume that the nodes in `nodes` are
-- contiguous, and prefer falling into the previous/next node if `pos` is on
-- mid's boundary, and mid is not linkable/interactie.
-- This way, we are more likely to return a node that can handle a new
-- snippet/is interactive.
-- * `"prefer_outside"` makes sense when the nodes are not contiguous, and we'd
-- like to find a position between two nodes.
-- This mode makes sense for finding the snippet a new snippet should be
-- inserted in, since we'd like to prefer inserting before/after a snippet, if
-- the position is ambiguous.
--
-- In general:
-- These options are useful for making this function more general: When
-- searching in the contiguous nodes of a snippet, we'd like this routine to
-- return any of them (obviously the one pos is inside/or on the border of, and
-- we'd like to prefer returning a node that can be linked), but in no case
-- fail.
-- However! when searching the top-level snippets with the intention of finding
-- the snippet/node a new snippet should be expanded inside, it seems better to
-- shift an existing snippet to the right/left than expand the new snippet
-- inside it (when the expand-point is on the boundary).
local function binarysearch_pos(
nodes,
pos,
respect_rgravs,
boundary_resolve_mode
)
local left = 1
local right = #nodes
-- actual search-routine from
-- https://github.com/Roblox/Wiki-Lua-Libraries/blob/master/StandardLibraries/BinarySearch.lua
if #nodes == 0 then
return nil, 1
end
while true do
local mid = left + math.floor((right - left) / 2)
local mid_mark = nodes[mid].mark
local ok, mid_from, mid_to = pcall(mid_mark.pos_begin_end_raw, mid_mark)
if not ok then
-- error while running this procedure!
-- return false (because I don't know how to do this with `error`
-- and the offending node).
-- (returning data instead of a message in `error` seems weird..)
return false, mid
end
if respect_rgravs then
-- if rgrav is set on either endpoint, the node considers its
-- endpoint to be the right, not the left edge.
-- We only want to work with left edges but since the right edge is
-- the left edge of the next column, this is not an issue :)
-- TODO: does this fail with multibyte characters???
if mid_mark:get_rgrav(-1) then
mid_from[2] = mid_from[2] + 1
end
if mid_mark:get_rgrav(1) then
mid_to[2] = mid_to[2] + 1
end
end
local cmp_mid_to = util.pos_cmp(pos, mid_to)
local cmp_mid_from = util.pos_cmp(pos, mid_from)
local cont_behind_mid, cont_before_mid =
boundary_resolve_mode(cmp_mid_to, cmp_mid_from, nodes[mid])
if cont_behind_mid then
-- make sure right-left becomes smaller.
left = mid + 1
if left > right then
return nil, mid + 1
end
elseif cont_before_mid then
-- continue search on left side
right = mid - 1
if left > right then
return nil, mid
end
else
-- greater-equal than mid_from, smaller or equal to mid_to => left edge
-- of pos is inside nodes[mid] :)
return nodes[mid], mid
end
end
end
-- a and b have to be in the same snippet, return their first (as seen from
-- them) common parent.
local function first_common_node(a, b)
local a_pos = a.absolute_position
local b_pos = b.absolute_position
-- last as seen from root.
local i = 0
local last_common = a.parent.snippet
-- invariant: last_common is parent of both a and b.
while (a_pos[i + 1] ~= nil) and a_pos[i + 1] == b_pos[i + 1] do
last_common = last_common:resolve_position(a_pos[i + 1])
i = i + 1
end
return last_common
end
-- roots at depth 0, children of root at depth 1, their children at 2, ...
local function snippettree_depth(snippet)
local depth = 0
while snippet.parent_node ~= nil do
snippet = snippet.parent_node.parent.snippet
depth = depth + 1
end
return depth
end
-- find the first common snippet a and b have on their respective unique paths
-- to the snippet-roots.
-- if no common ancestor exists (ie. a and b are roots of their buffers'
-- forest, or just in different trees), return nil.
-- in both cases, the paths themselves are returned as well.
-- The common ancestor is included in the paths, except if there is none.
-- Instead of storing the snippets in the paths, they are represented by the
-- node which contains the next-lower snippet in the path (or `from`/`to`, if it's
-- the first node of the path)
-- This is a bit complicated, but this representation contains more information
-- (or, more easily accessible information) than storing snippets: the
-- immediate parent of the child along the path cannot be easily retrieved if
-- the snippet is stored, but the snippet can be easily retrieved if the child
-- is stored (.parent.snippet).
-- And, so far this is pretty specific to refocus, and thus modeled so there is
-- very little additional work in that method.
-- At most one of a,b may be nil.
local function first_common_snippet_ancestor_path(a, b)
local a_path = {}
local b_path = {}
-- general idea: we find the depth of a and b, walk upward with the deeper
-- one until we find its first ancestor with the same depth as the less
-- deep snippet, and then follow both paths until they arrive at the same
-- snippet (or at the root of their respective trees).
-- if either is nil, we treat it like it's one of the roots (the code will
-- behave correctly this way, and return an empty path for the nil-node,
-- and the correct path for the non-nil one).
local a_depth = a ~= nil and snippettree_depth(a) or 0
local b_depth = b ~= nil and snippettree_depth(b) or 0
-- bit subtle: both could be 0, but one could be nil.
-- deeper should not be nil! (this allows us to do the whole walk for the
-- non-nil node in the first for-loop, as opposed to needing some special
-- handling).
local deeper, deeper_path, other, other_path
if b == nil or (a ~= nil and a_depth > b_depth) then
deeper = a
other = b
deeper_path = a_path
other_path = b_path
else
-- we land here if `b ~= nil and (a == nil or a_depth >= b_depth)`, so
-- exactly what we want.
deeper = b
other = a
deeper_path = b_path
other_path = a_path
end
for _ = 1, math.abs(a_depth - b_depth) do
table.insert(deeper_path, deeper.parent_node)
deeper = deeper.parent_node.parent.snippet
end
-- here: deeper and other are at the same depth.
-- If we walk upwards one step at a time, they will meet at the same
-- parent, or hit their respective roots.
-- deeper can't be nil, if other is, we are done here and can return the
-- paths (and there is no shared node)
if other == nil then
return nil, a_path, b_path
end
-- beyond here, deeper and other are not nil.
while deeper ~= other do
if deeper.parent_node == nil then
-- deeper is at depth 0 => other as well => both are roots.
return nil, a_path, b_path
end
table.insert(deeper_path, deeper.parent_node)
table.insert(other_path, other.parent_node)
-- walk one step towards root.
deeper = deeper.parent_node.parent.snippet
other = other.parent_node.parent.snippet
end
-- either one will do here.
return deeper, a_path, b_path
end
-- removes focus from `from` and upwards up to the first common ancestor
-- (node!) of `from` and `to`, and then focuses nodes between that ancestor and
-- `to`.
-- Requires that `from` is currently entered/focused, and that no snippet
-- between `to` and its root is invalid.
local function refocus(from, to)
if from == nil and to == nil then
-- absolutely nothing to do, should not happen.
return
end
-- pass nil if from/to is nil.
-- if either is nil, first_common_node is nil, and the corresponding list empty.
local first_common_snippet, from_snip_path, to_snip_path =
first_common_snippet_ancestor_path(
from and from.parent.snippet,
to and to.parent.snippet
)
-- we want leave/enter_path to be s.t. leaving/entering all nodes between
-- each entry and its snippet (and the snippet itself) will leave/enter all
-- nodes between the first common snippet (or the root-snippet) and
-- from/to.
-- Then, the nodes between the first common node and the respective
-- entrypoints (also nodes) into the first common snippet will have to be
-- left/entered, which is handled by final_leave_/first_enter_/common_node.
-- from, to are not yet in the paths.
table.insert(from_snip_path, 1, from)
table.insert(to_snip_path, 1, to)
-- determine how far to leave: if there is a common snippet, only up to the
-- first common node of from and to, otherwise leave the one snippet, and
-- enter the other completely.
local final_leave_node, first_enter_node, common_node
if first_common_snippet then
-- there is a common snippet => there is a common node => we have to
-- set final_leave_node, first_enter_node, and common_node.
final_leave_node = from_snip_path[#from_snip_path]
first_enter_node = to_snip_path[#to_snip_path]
common_node = first_common_node(first_enter_node, final_leave_node)
-- Also remove these last nodes from the lists, their snippet is not
-- supposed to be left entirely.
from_snip_path[#from_snip_path] = nil
to_snip_path[#to_snip_path] = nil
end
-- now do leave/enter, set no_move on all operations.
-- if one of from/to was nil, there are no leave/enter-operations done for
-- it (from/to_snip_path is {}, final_leave/first_enter_* is nil).
-- leave_children on all from-nodes except the original from.
if #from_snip_path > 0 then
local ok1, ok2
if from.type == types.exitNode then
ok1 = pcall(from.input_leave, from, true)
ok2 = true
else
-- we know that the first node is from.
ok1 = pcall(leave_nodes_between, from.parent.snippet, from, true)
-- leave_nodes_between does not affect snippet, so that has to be left
-- here.
-- snippet does not have input_leave_children, so only input_leave
-- needs to be called.
ok2 = pcall(
from.parent.snippet.input_leave,
from.parent.snippet,
true
)
end
if not ok1 or not ok2 then
from.parent.snippet:remove_from_jumplist()
end
end
for i = 2, #from_snip_path do
local node = from_snip_path[i]
local ok1, ok2, ok3
ok1 = pcall(node.input_leave_children, node)
if node.type == types.exitNode then
ok2 = pcall(node.input_leave, node, true)
ok3 = true
else
ok2 = pcall(leave_nodes_between, node.parent.snippet, node, true)
ok3 = pcall(
node.parent.snippet.input_leave,
node.parent.snippet,
true
)
end
if not ok1 or not ok2 or not ok3 then
from.parent.snippet:remove_from_jumplist()
end
end
-- this leave, and the following enters should be safe: the path to `to`
-- was verified via extmarks_valid (precondition).
if common_node and final_leave_node then
-- if the final_leave_node is from, its children are not active (which
-- stems from the requirement that from is the currently active node),
-- and so don't have to be left.
if final_leave_node ~= from then
final_leave_node:input_leave_children()
end
leave_nodes_between(common_node, final_leave_node, true)
end
if common_node and first_enter_node then
-- In general we assume that common_node is active when we are here.
-- This may not be the case if we are currently inside the i(0) or
-- i(-1), since the snippet might be the common node and in this case,
-- it is inactive.
-- This means that, if we want to enter a non-exitNode, we have to
-- explicitly activate the snippet for all jumps to behave correctly.
-- (if we enter a i(0)/i(-1), this is not necessary, of course).
if
final_leave_node.type == types.exitNode
and first_enter_node.type ~= types.exitNode
then
common_node:input_enter(true)
end
-- symmetrically, entering an i(0)/i(-1) requires leaving the snippet.
if
final_leave_node.type ~= types.exitNode
and first_enter_node.type == types.exitNode
then
common_node:input_leave(true)
end
enter_nodes_between(common_node, first_enter_node, true)
-- if the `first_enter_node` is already `to` (occurs if `to` is in the
-- common snippet of to and from), we should not enter its children.
-- (we only want to `input_enter` to.)
if first_enter_node ~= to then
first_enter_node:input_enter_children()
end
end
-- same here, input_enter_children has to be called manually for the
-- to-nodes of the path we are entering (since enter_nodes_between does not
-- call it for the child-node).
for i = #to_snip_path, 2, -1 do
local node = to_snip_path[i]
if node.type ~= types.exitNode then
node.parent.snippet:input_enter(true)
enter_nodes_between(node.parent.snippet, node, true)
else
node:input_enter(true)
end
node:input_enter_children()
end
if #to_snip_path > 0 then
if to.type ~= types.exitNode then
to.parent.snippet:input_enter(true)
else
to.parent.snippet:input_leave(true)
end
enter_nodes_between(to.parent.snippet, to, true)
end
-- it may be that we only leave nodes in this process (happens if to is a
-- parent of from).
-- If that is the case, we will not explicitly focus on to, and it may be
-- that focus is even lost if it was focused previously (leave may trigger
-- update, update may change focus)
-- To prevent this, just call focus here, which is pretty close to a noop
-- if to is already focused.
if to then
to:focus()
end
end
local function generic_extmarks_valid(node, child)
-- valid if
-- - extmark-extents match.
-- - current choice is valid
local ok1, self_from, self_to =
pcall(node.mark.pos_begin_end_raw, node.mark)
local ok2, child_from, child_to =
pcall(child.mark.pos_begin_end_raw, child.mark)
if
not ok1
or not ok2
or util.pos_cmp(self_from, child_from) ~= 0
or util.pos_cmp(self_to, child_to) ~= 0
then
return false
end
return child:extmarks_valid()
end
-- returns: * the smallest known snippet `pos` is inside.
-- * the list of other snippets inside the snippet of this smallest
-- node
-- * the index this snippet would be at if inserted into that list
-- * the node of this snippet pos is on.
local function snippettree_find_undamaged_node(pos, opts)
local prev_parent, child_indx, found_parent
local prev_parent_children =
session.snippet_roots[vim.api.nvim_get_current_buf()]
while true do
-- false: don't respect rgravs.
-- Prefer inserting the snippet outside an existing one.
found_parent, child_indx = binarysearch_pos(
prev_parent_children,
pos,
opts.tree_respect_rgravs,
opts.tree_preference
)
if found_parent == false then
-- if the procedure returns false, there was an error getting the
-- position of a node (in this case, that node is a snippet).
-- The position of the offending snippet is returned in child_indx,
-- and we can remove it here.
prev_parent_children[child_indx]:remove_from_jumplist()
elseif found_parent ~= nil and not found_parent:extmarks_valid() then
-- found snippet damaged (the idea to sidestep the damaged snippet,
-- even if no error occurred _right now_, is to ensure that we can
-- input_enter all the nodes along the insertion-path correctly).
found_parent:remove_from_jumplist()
-- continue again with same parent, but one less snippet in its
-- children => shouldn't cause endless loop.
elseif found_parent == nil then
break
else
prev_parent = found_parent
-- can index prev_parent, since found_parent is not nil, and
-- assigned to prev_parent.
prev_parent_children = prev_parent.child_snippets
end
end
local node
if prev_parent then
-- if found, find node to insert at, prefer receiving a linkable node.
node = prev_parent:node_at(pos, opts.snippet_mode)
end
return prev_parent, prev_parent_children, child_indx, node
end
local function root_path(node)
local path = {}
while node do
local node_snippet = node.parent.snippet
local snippet_node_path = get_nodes_between(node_snippet, node)
-- get_nodes_between gives parent -> node, but we need
-- node -> parent => insert back to front.
for i = #snippet_node_path, 1, -1 do
table.insert(path, snippet_node_path[i])
end
-- parent not in get_nodes_between.
table.insert(path, node_snippet)
node = node_snippet.parent_node
end
return path
end
-- adjust rgravs of siblings of the node with indx child_from_indx in nodes.
local function nodelist_adjust_rgravs(
nodes,
child_from_indx,
child_endpoint,
direction,
rgrav,
nodes_adjacent
)
-- only handle siblings, not the node with child_from_indx itself.
local i = child_from_indx
local node = nodes[i]
while node do
local direction_node_endpoint = node.mark:get_endpoint(direction)
if util.pos_equal(direction_node_endpoint, child_endpoint) then
-- both endpoints of node are on top of child_endpoint (we wouldn't
-- be in the loop with `node` if the -direction-endpoint didn't
-- match), so update rgravs of the entire subtree to match rgrav
node:subtree_set_rgrav(rgrav)
else
-- either assume that they are adjacent, or check.
if
nodes_adjacent
or util.pos_equal(
node.mark:get_endpoint(-direction),
child_endpoint
)
then
-- only the -direction-endpoint matches child_endpoint, adjust its
-- position and break the loop (don't need to look at any other
-- siblings).
node:subtree_set_pos_rgrav(child_endpoint, direction, rgrav)
end
break
end
i = i + direction
node = nodes[i]
end
end
return {
subsnip_init_children = subsnip_init_children,
init_child_positions_func = init_child_positions_func,
make_args_absolute = make_args_absolute,
wrap_args = wrap_args,
wrap_context = wrap_context,
get_nodes_between = get_nodes_between,
leave_nodes_between = leave_nodes_between,
enter_nodes_between = enter_nodes_between,
select_node = select_node,
print_dict = print_dict,
init_node_opts = init_node_opts,
snippet_extend_context = snippet_extend_context,
linkable_node = linkable_node,
binarysearch_pos = binarysearch_pos,
binarysearch_preference = binarysearch_preference,
refocus = refocus,
generic_extmarks_valid = generic_extmarks_valid,
snippettree_find_undamaged_node = snippettree_find_undamaged_node,
interactive_node = interactive_node,
root_path = root_path,
nodelist_adjust_rgravs = nodelist_adjust_rgravs,
}

View File

@ -0,0 +1,132 @@
local jsregexp_compile_safe = require("luasnip.util.jsregexp")
-- generate nil-opts-instances here, and save them.
-- This is to prevent generating 100s of the exact same function.
local default_match_pattern, default_match_plain, default_match_vim
local function apply_common_opts(line_to_cursor, opts)
if opts and opts.max_len then
return line_to_cursor:sub(#line_to_cursor - opts.max_len + 1)
else
return line_to_cursor
end
end
-- these functions get the line up to the cursor, the trigger, and then
-- determine whether the trigger matches the current line.
-- If the trigger does not match, the functions shall return nil, otherwise
-- the matching substring and the list of captures (empty table if there aren't
-- any).
local function match_plain(_, opts)
if opts == nil then
return default_match_plain
end
return function(line_to_cursor, trigger)
line_to_cursor = apply_common_opts(line_to_cursor, opts)
if
line_to_cursor:sub(#line_to_cursor - #trigger + 1, #line_to_cursor)
== trigger
then
-- no captures for plain trigger.
return trigger, {}
else
return nil
end
end
end
default_match_plain = match_plain(nil, {})
local function match_pattern(_, opts)
if opts == nil then
return default_match_pattern
end
return function(line_to_cursor, trigger)
line_to_cursor = apply_common_opts(line_to_cursor, opts)
-- 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
end
default_match_pattern = match_pattern(nil, {})
local ecma_engine
if jsregexp_compile_safe then
ecma_engine = function(trig, opts)
local trig_compiled, err_maybe = jsregexp_compile_safe(trig .. "$", "")
if not trig_compiled then
error(("Error while compiling regex: %s"):format(err_maybe))
end
return function(line_to_cursor, _)
line_to_cursor = apply_common_opts(line_to_cursor, opts)
-- get first (very likely only, since we appended the "$") match.
local match = trig_compiled(line_to_cursor)[1]
if match then
-- return full match, and all groups.
return line_to_cursor:sub(match.begin_ind), match.groups
else
return nil
end
end
end
else
ecma_engine = function(x, opts)
return match_plain(x, opts)
end
end
local function match_vim(_, opts)
if opts == nil then
return default_match_vim
end
return function(line_to_cursor, trigger)
line_to_cursor = apply_common_opts(line_to_cursor, opts)
local matchlist = vim.fn.matchlist(line_to_cursor, trigger .. "$")
if #matchlist > 0 then
local groups = {}
for i = 2, 10 do
-- PROBLEM: vim does not differentiate between an empty ("")
-- and a missing capture.
-- Since we need to differentiate between the two (Check `:h
-- luasnip-variables-lsp-variables`), we assume, here, that an
-- empty string is an unmatched group.
groups[i - 1] = matchlist[i] ~= "" and matchlist[i] or nil
end
return matchlist[1], groups
else
return nil
end
end
end
default_match_vim = match_vim(nil, {})
return {
plain = match_plain,
pattern = match_pattern,
ecma = ecma_engine,
vim = match_vim,
}

View File

@ -0,0 +1,35 @@
local snippet_collection = require("luasnip.session.snippet_collection")
local M = {}
local refresh_enqueued = false
local next_refresh_fts = {}
function M.refresh_notify(ft)
next_refresh_fts[ft] = true
if not refresh_enqueued then
vim.schedule(function()
for enq_ft, _ in pairs(next_refresh_fts) do
snippet_collection.refresh_notify(enq_ft)
end
next_refresh_fts = {}
refresh_enqueued = false
end)
refresh_enqueued = true
end
end
local clean_enqueued = false
function M.clean_invalidated()
if not clean_enqueued then
vim.schedule(function()
snippet_collection.clean_invalidated({ inv_limit = 100 })
end)
end
clean_enqueued = true
end
return M

View File

@ -0,0 +1,52 @@
-- used to store values like current nodes or the active node for autocommands.
local M = {}
M.ft_redirect = {}
setmetatable(M.ft_redirect, {
__index = function(table, key)
-- no entry for this ft(key), set it to avoid calls on each expand for
-- this filetype.
local val = { key }
rawset(table, key, val)
return val
end,
})
M.current_nodes = {}
-- roots of snippet-trees, per-buffer.
-- snippet_roots[n] => list of snippet-roots in buffer n.
M.snippet_roots = setmetatable({}, {
-- create missing lists automatically.
__index = function(t, k)
local new_t = {}
rawset(t, k, new_t)
return new_t
end,
})
M.ns_id = vim.api.nvim_create_namespace("Luasnip")
M.active_choice_nodes = {}
-- only here for overview.
M.latest_load_ft = nil
M.last_expand_snip = nil
M.last_expand_opts = nil
-- jump_active is set while luasnip moves the cursor, prevents
-- (for example) updating dependents or deleting a snippet via
-- exit_out_of_region while jumping.
-- init with false, it will be set by (eg.) ls.jump().
M.jump_active = false
-- initial value, might be overwritten immediately.
-- No danger of overwriting user-config, since this has to be loaded to allow
-- overwriting.
M.config = require("luasnip.default_config")
M.loaded_fts = {}
function M.get_snip_env()
return M.config.snip_env
end
return M

View File

@ -0,0 +1,328 @@
local source = require("luasnip.session.snippet_collection.source")
local u_table = require("luasnip.util.table")
local auto_creating_tables =
require("luasnip.util.auto_table").warn_depth_autotable
local session = require("luasnip.session")
-- store snippets by some key.
-- also ordered by filetype, eg.
-- {
-- key = {
-- ft1 = {...},
-- ft2 = {...}
-- }
-- }
local M = {
invalidated_count = 0,
}
local by_key = {}
-- stores snippets/autosnippets by priority.
local by_prio = {
snippets = {
-- stores sorted keys, eg 1=1000, 2=1010, 3=1020,..., used for
-- quick iterating.
order = {},
},
autosnippets = {
order = {},
},
}
-- this isn't in util/util.lua due to circular dependencies. Would be cleaner
-- to include it there, but it's alright to keep here for now.
--
-- this is linear, binary search would certainly be nicer, but for our
-- applications this should easily be enough.
local function insert_sorted_unique(t, k)
local tbl_len = #t
local i = 1
-- k does not yet exist in table, find first i so t[i] > k.
for _ = 1, tbl_len do
if t[i] > k then
break
end
i = i + 1
end
-- shift all t[j] with j > i back by one.
for j = tbl_len, i, -1 do
t[j + 1] = t[j]
end
t[i] = k
end
local by_prio_snippets_mt = {
__index = function(s, k)
-- make new tables as they are indexed
return auto_creating_tables(s, k, 3)
end,
__newindex = function(t, k, v)
-- update priority-order as well.
insert_sorted_unique(t.order, k)
rawset(t, k, v)
end,
}
-- metatable for the by_prio table used when by_prio.type[prio] is reset
-- create here so that it can be shared and only has to be created once
local prio_mt2 = {
__index = function(s, k)
-- make new tables as they are indexed
return auto_creating_tables(s, k, 2)
end,
}
setmetatable(by_prio.snippets, by_prio_snippets_mt)
setmetatable(by_prio.autosnippets, by_prio_snippets_mt)
-- iterate priorities, high to low.
local function prio_iter(type)
local order = by_prio[type].order
local i = #order + 1
return function()
i = i - 1
if i > 0 then
return by_prio[type][order[i]]
end
return nil
end
end
local by_ft = {
snippets = {},
autosnippets = {},
}
local by_ft_snippets_mt = {
__index = function(s, k)
return auto_creating_tables(s, k, 2)
end,
}
setmetatable(by_ft.snippets, by_ft_snippets_mt)
setmetatable(by_ft.autosnippets, by_ft_snippets_mt)
local by_id = setmetatable({}, {
-- make by_id-table weak (v).
-- this means it won't be necessary to explicitly nil values (snippets) in
-- this table.
__mode = "v",
})
-- ft: any filetype, optional.
function M.clear_snippets(ft)
if ft then
-- remove all ft-(auto)snippets for all priorities.
-- set to empty table so we won't need to rebuild/clear the order-table.
for _, prio in ipairs(by_prio.snippets.order) do
by_prio.snippets[prio][ft] = {}
end
for _, prio in ipairs(by_prio.autosnippets.order) do
by_prio.autosnippets[prio][ft] = {}
end
by_ft.snippets[ft] = nil
by_ft.autosnippets[ft] = nil
for key, _ in pairs(by_key) do
by_key[key][ft] = nil
end
else
-- remove all (auto)snippets for all priorities.
for _, prio in ipairs(by_prio.snippets.order) do
by_prio.snippets[prio] = {}
setmetatable(by_prio.snippets[prio], prio_mt2)
end
for _, prio in ipairs(by_prio.autosnippets.order) do
by_prio.autosnippets[prio] = {}
setmetatable(by_prio.autosnippets[prio], prio_mt2)
end
by_key = {}
by_ft.snippets = {}
setmetatable(by_ft.snippets, by_ft_snippets_mt)
by_ft.autosnippets = {}
setmetatable(by_ft.autosnippets, by_ft_snippets_mt)
end
end
function M.match_snippet(line, fts, type)
local expand_params
for prio_by_ft in prio_iter(type) do
for _, ft in ipairs(fts) do
for _, snip in ipairs(prio_by_ft[ft] or {}) do
expand_params = snip:matches(line)
if expand_params then
-- return matching snippet and table with expand-parameters.
return snip, expand_params
end
end
end
end
return nil
end
local function without_invalidated(snippets_by_ft)
local new_snippets = {}
for ft, ft_snippets in pairs(snippets_by_ft) do
new_snippets[ft] = {}
for _, snippet in ipairs(ft_snippets) do
if not snippet.invalidated then
table.insert(new_snippets[ft], snippet)
end
end
end
return new_snippets
end
function M.clean_invalidated(opts)
if opts.inv_limit then
if M.invalidated_count <= opts.inv_limit then
return
end
end
-- remove invalidated snippets from all tables.
for _, type_snippets in pairs(by_prio) do
for key, prio_snippets in pairs(type_snippets) do
if key ~= "order" then
type_snippets[key] = without_invalidated(prio_snippets)
setmetatable(type_snippets[key], prio_mt2)
end
end
end
for type, type_snippets in pairs(by_ft) do
by_ft[type] = without_invalidated(type_snippets)
setmetatable(by_ft[type], by_ft_snippets_mt)
end
for key, key_snippets in pairs(by_key) do
by_key[key] = without_invalidated(key_snippets)
end
M.invalidated_count = 0
end
local function invalidate_addables(addables_by_ft)
for _, addables in pairs(addables_by_ft) do
for _, addable in ipairs(addables) do
for _, expandable in ipairs(addable:retrieve_all()) do
expandable:invalidate()
end
end
end
M.clean_invalidated({ inv_limit = 100 })
end
local current_id = 0
-- snippets like {ft1={<snippets>}, ft2={<snippets>}}, opts should be properly
-- initialized with default values.
function M.add_snippets(snippets, opts)
for ft, ft_snippets in pairs(snippets) do
for _, addable in ipairs(ft_snippets) do
for _, snip in ipairs(addable:retrieve_all()) do
local snip_prio = opts.override_priority
or (snip.priority and snip.priority)
or opts.default_priority
or 1000
-- if snippetType undefined by snippet, take default value from opts
local snip_type = snip.snippetType ~= nil and snip.snippetType
or opts.type
assert(
snip_type == "autosnippets" or snip_type == "snippets",
"snippetType must be either 'autosnippets' or 'snippets', was "
.. vim.inspect(snip_type)
)
local snip_ft = snip.filetype or ft
snip.id = current_id
current_id = current_id + 1
-- do the insertion
table.insert(by_prio[snip_type][snip_prio][snip_ft], snip)
table.insert(by_ft[snip_type][snip_ft], snip)
by_id[snip.id] = snip
-- set source if it is available.
if snip._source then
source.set(snip, snip._source)
end
end
end
end
if opts.key then
if by_key[opts.key] then
invalidate_addables(by_key[opts.key])
end
by_key[opts.key] = snippets
end
end
-- specialized copy functions to not loose performance on ifs when copying
-- and to be able to specify when pairs or ipairs is used
local function copy_by_ft_type_ft(tab)
local r = {}
for k, v in ipairs(tab) do
r[k] = v
end
return r
end
local function copy_by_ft_type(tab)
local r = {}
for k, v in pairs(tab) do
r[k] = copy_by_ft_type_ft(v)
end
return r
end
-- ft may be nil, type not.
function M.get_snippets(ft, type)
if ft then
return copy_by_ft_type_ft(by_ft[type][ft])
else
return copy_by_ft_type(by_ft[type])
end
end
function M.get_id_snippet(id)
return by_id[id]
end
local function get_all_snippet_fts()
local ft_set = {}
for ft, _ in pairs(by_ft.snippets) do
ft_set[ft] = true
end
for ft, _ in pairs(by_ft.autosnippets) do
ft_set[ft] = true
end
return u_table.set_to_list(ft_set)
end
-- modules that want to call refresh_notify probably also want to notify others
-- of adding those snippets => put those functions into the same module.
function M.refresh_notify(ft_or_nil)
local fts = ft_or_nil and { ft_or_nil } or get_all_snippet_fts()
for _, ft in ipairs(fts) do
session.latest_load_ft = ft
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "LuasnipSnippetsAdded", modeline = false }
)
end
end
return M

View File

@ -0,0 +1,38 @@
local id_to_source = {}
local M = {}
function M.from_debuginfo(debuginfo)
assert(debuginfo.source, "debuginfo contains source")
assert(
debuginfo.source:match("^@"),
"debuginfo-source is a file: " .. debuginfo.source
)
return {
-- omit leading '@'.
file = debuginfo.source:sub(2),
line = debuginfo.currentline,
}
end
function M.from_location(file, opts)
assert(file, "source needs file")
opts = opts or {}
return { file = file, line = opts.line, line_end = opts.line_end }
end
function M.set(snippet, source)
-- snippets only get their id after being added, make sure this is the
-- case.
assert(snippet.id, "snippet has an id")
id_to_source[snippet.id] = source
end
function M.get(snippet)
return id_to_source[snippet.id]
end
return M

View File

@ -0,0 +1,208 @@
local util = require("luasnip.util.util")
local select_util = require("luasnip.util.select")
local time_util = require("luasnip.util.time")
local lazy_vars = {}
-- Variables defined in https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables
-- Inherited from TextMate
function lazy_vars.TM_FILENAME()
return vim.fn.expand("%:t")
end
function lazy_vars.TM_FILENAME_BASE()
return vim.fn.expand("%:t:s?\\.[^\\.]\\+$??")
end
function lazy_vars.TM_DIRECTORY()
return vim.fn.expand("%:p:h")
end
function lazy_vars.TM_FILEPATH()
return vim.fn.expand("%:p")
end
-- Vscode only
function lazy_vars.CLIPBOARD() -- The contents of your clipboard
return vim.fn.getreg('"', 1, true)
end
local function buf_to_ws_part()
local LSP_WORSKPACE_PARTS = "LSP_WORSKPACE_PARTS"
local ok, ws_parts = pcall(vim.api.nvim_buf_get_var, 0, LSP_WORSKPACE_PARTS)
if not ok then
local file_path = vim.fn.expand("%:p")
for _, ws in pairs(vim.lsp.buf.list_workspace_folders()) do
if file_path:find(ws, 1, true) == 1 then
ws_parts = { ws, file_path:sub(#ws + 2, -1) }
break
end
end
-- If it can't be extracted from lsp, then we use the file path
if not ok and not ws_parts then
ws_parts = { vim.fn.expand("%:p:h"), vim.fn.expand("%:p:t") }
end
vim.api.nvim_buf_set_var(0, LSP_WORSKPACE_PARTS, ws_parts)
end
return ws_parts
end
function lazy_vars.RELATIVE_FILEPATH() -- The relative (to the opened workspace or folder) file path of the current document
return buf_to_ws_part()[2]
end
function lazy_vars.WORKSPACE_FOLDER() -- The path of the opened workspace or folder
return buf_to_ws_part()[1]
end
function lazy_vars.WORKSPACE_NAME() -- The name of the opened workspace or folder
local parts = vim.split(buf_to_ws_part()[1] or "", "[\\/]")
return parts[#parts]
end
-- DateTime Related
function lazy_vars.CURRENT_YEAR()
return os.date("%Y")
end
function lazy_vars.CURRENT_YEAR_SHORT()
return os.date("%y")
end
function lazy_vars.CURRENT_MONTH()
return os.date("%m")
end
function lazy_vars.CURRENT_MONTH_NAME()
return os.date("%B")
end
function lazy_vars.CURRENT_MONTH_NAME_SHORT()
return os.date("%b")
end
function lazy_vars.CURRENT_DATE()
return os.date("%d")
end
function lazy_vars.CURRENT_DAY_NAME()
return os.date("%A")
end
function lazy_vars.CURRENT_DAY_NAME_SHORT()
return os.date("%a")
end
function lazy_vars.CURRENT_HOUR()
return os.date("%H")
end
function lazy_vars.CURRENT_MINUTE()
return os.date("%M")
end
function lazy_vars.CURRENT_SECOND()
return os.date("%S")
end
function lazy_vars.CURRENT_SECONDS_UNIX()
return tostring(os.time())
end
function lazy_vars.CURRENT_TIMEZONE_OFFSET()
return time_util
.get_timezone_offset(os.time())
:gsub("([+-])(%d%d)(%d%d)$", "%1%2:%3")
end
-- For inserting random values
math.randomseed(os.time())
function lazy_vars.RANDOM()
return string.format("%06d", math.random(999999))
end
function lazy_vars.RANDOM_HEX()
return string.format("%06x", math.random(16777216)) --16^6
end
function lazy_vars.UUID()
local random = math.random
local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
local out
local function subs(c)
local v = (((c == "x") and random(0, 15)) or random(8, 11))
return string.format("%x", v)
end
out = template:gsub("[xy]", subs)
return out
end
function lazy_vars.LINE_COMMENT()
return util.buffer_comment_chars()[1]
end
function lazy_vars.BLOCK_COMMENT_START()
return util.buffer_comment_chars()[2]
end
function lazy_vars.BLOCK_COMMENT_END()
return util.buffer_comment_chars()[3]
end
-- These are the vars that have to be populated once the snippet starts to avoid any issue
local function eager_vars(info)
local vars = {}
local pos = info.pos
vars.TM_CURRENT_LINE =
vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1]
vars.TM_CURRENT_WORD = util.word_under_cursor(pos, vars.TM_CURRENT_LINE)
vars.TM_LINE_INDEX = tostring(pos[1])
vars.TM_LINE_NUMBER = tostring(pos[1] + 1)
vars.LS_SELECT_RAW, vars.LS_SELECT_DEDENT, vars.TM_SELECTED_TEXT =
select_util.retrieve()
-- These are for backward compatibility, for now on all builtins that are not part of TM_ go in LS_
vars.SELECT_RAW, vars.SELECT_DEDENT =
vars.LS_SELECT_RAW, vars.LS_SELECT_DEDENT
for i, cap in ipairs(info.captures) do
vars["LS_CAPTURE_" .. i] = cap
end
vars.LS_TRIGGER = info.trigger
return vars
end
local builtin_ns = { SELECT = true, LS = true }
for name, _ in pairs(lazy_vars) do
local parts = vim.split(name, "_")
if #parts > 1 then
builtin_ns[parts[1]] = true
end
end
local _is_table = {
TM_SELECTED_TEXT = true,
SELECT_RAW = true,
SELECT_DEDENT = true,
}
return {
is_table = function(key)
-- variables generated at runtime by treesitter-postfix.
if
key:match("LS_TSCAPTURE_.*")
or key == "LS_TSPOSTFIX_MATCH"
or key == "LS_TSPOSTFIX_DATA"
then
return true
end
return _is_table[key] or false
end,
vars = lazy_vars,
init = eager_vars,
builtin_ns = builtin_ns,
}

View File

@ -0,0 +1,54 @@
-- depth specifies how many levels under this table should be allowed to index
-- throug this metamethod
-- set depth to 0 to disable checking
-- Acknowledgment: This is (maybe more than) inspired by
-- https://lua-users.org/wiki/AutomagicTables so special thanks to
-- Thomas Wrensch and Rici Lake for sharing their ideas on this topic.
local function auto_creating_tables_warn_depth(self, key, depth)
local t = {}
assert(depth ~= 1, "don't index at that level")
setmetatable(t, {
-- creating a new function on each time (could be shared) isn't that
-- nice. Nonetheless this shouldn't be too bad, as these are only
-- created twice (auto+snippet) per ft and twice for each prio,ft
-- combination
__index = function(s, k)
return auto_creating_tables_warn_depth(s, k, depth - 1)
end,
})
self[key] = t
return t
end
local function auto_creating_tables(self, key, depth)
local t = {}
if depth ~= 1 then
setmetatable(t, {
__index = function(s, k)
return auto_creating_tables(s, k, depth - 1)
end,
})
end
self[key] = t
return t
end
local M = {}
function M.autotable(max_depth, opts)
opts = opts or {}
local warn = vim.F.if_nil(opts.warn, false)
local auto_table_func = warn and auto_creating_tables_warn_depth
or auto_creating_tables
return setmetatable({}, {
__index = function(s, k)
return auto_table_func(s, k, max_depth - 1)
end,
})
end
M.warn_depth_autotable = auto_creating_tables_warn_depth
return M

View File

@ -0,0 +1,63 @@
local Dictionary = {}
local function new(o)
return setmetatable(o or {}, {
__index = Dictionary,
})
end
function Dictionary:set(path, value)
-- Insp(path)
-- print("val: ", value)
local current_table = self
for i = 1, #path - 1 do
local crt_key = path[i]
if not current_table[crt_key] then
current_table[crt_key] = {}
end
current_table = current_table[crt_key]
end
current_table[path[#path]] = value
end
function Dictionary:get(path)
local current_table = self
for _, v in ipairs(path) do
if not current_table[v] then
return nil
end
current_table = current_table[v]
end
-- may not be a table.
return current_table
end
function Dictionary:find_all(path, key)
local res = {}
local to_search = { self:get(path) }
if not to_search[1] then
return nil
end
-- weird hybrid of depth- and breadth-first search for key, collect values in res.
local search_index = 1
local search_size = 1
while search_size > 0 do
for k, v in pairs(to_search[search_index]) do
if k == key then
res[#res + 1] = v
else
to_search[search_index + search_size] = v
search_size = search_size + 1
end
end
search_index = search_index + 1
search_size = search_size - 1
end
return res
end
return {
new = new,
}

View File

@ -0,0 +1,263 @@
local tbl_util = require("luasnip.util.table")
local autotable = require("luasnip.util.auto_table").autotable
local DirectedGraph = {}
-- set __index directly in DirectedGraph, otherwise each DirectedGraph-object would have its'
-- own metatable (one more table around), which would not be bad, but
-- unnecessary nonetheless.
DirectedGraph.__index = DirectedGraph
local Vertex = {}
Vertex.__index = Vertex
local function new_graph()
return setmetatable({
-- all vertices of this graph.
vertices = {},
}, DirectedGraph)
end
local function new_vertex()
return setmetatable({
-- vertices this vertex has an edge from/to.
-- map[vert -> bool]
incoming_edge_verts = {},
outgoing_edge_verts = {},
}, Vertex)
end
---Add new vertex to the DirectedGraph
---@return table: the generated vertex, to be used in `set_edge`, for example.
function DirectedGraph:add_vertex()
local vert = new_vertex()
table.insert(self.vertices, vert)
return vert
end
---Remove vertex and its edges from DirectedGraph.
---@param v table: the vertex.
function DirectedGraph:clear_vertex(v)
if not vim.tbl_contains(self.vertices, v) then
-- vertex does not belong to this graph. Maybe throw error/make
-- condition known?
return
end
-- remove outgoing..
for outgoing_edge_vert, _ in pairs(v.outgoing_edge_verts) do
self:clear_edge(v, outgoing_edge_vert)
end
-- ..and incoming edges with v from the graph.
for incoming_edge_vert, _ in pairs(v.incoming_edge_verts) do
self:clear_edge(incoming_edge_vert, v)
end
end
---Add edge from v1 to v2
---@param v1 table: vertex in the graph.
---@param v2 table: vertex in the graph.
function DirectedGraph:set_edge(v1, v2)
if v1.outgoing_edge_verts[v2] then
-- the edge already exists. Don't return an error, for now.
return
end
-- link vertices.
v1.outgoing_edge_verts[v2] = true
v2.incoming_edge_verts[v1] = true
end
---Remove edge from v1 to v2
---@param v1 table: vertex in the graph.
---@param v2 table: vertex in the graph.
function DirectedGraph:clear_edge(v1, v2)
assert(v1.outgoing_edge_verts[v2], "nonexistent edge cannot be removed.")
-- unlink vertices.
v1.outgoing_edge_verts[v2] = nil
v2.incoming_edge_verts[v1] = nil
end
---Find and return verts with indegree 0.
---@param graph table: graph.
---@return table of vertices.
local function source_verts(graph)
local indegree_0_verts = {}
for _, vert in ipairs(graph.vertices) do
if vim.tbl_count(vert.incoming_edge_verts) == 0 then
table.insert(indegree_0_verts, vert)
end
end
return indegree_0_verts
end
---Copy graph.
---@param graph table: graph.
---@return table,table: copied graph and table for mapping copied node to
---original node(original_vert[some_vert_from_copy] -> corresponding original
---vert).
local function graph_copy(graph)
local copy = vim.deepcopy(graph)
local original_vert = {}
for i, copy_vert in ipairs(copy.vertices) do
original_vert[copy_vert] = graph.vertices[i]
end
return copy, original_vert
end
---Generate a (it's not necessarily unique) topological sorting of this graphs
---vertices.
---https://en.wikipedia.org/wiki/Topological_sorting, this uses Kahn's Algorithm.
---@return table|nil: sorted vertices of this graph, nil if there is no
---topological sorting (eg. if the graph has a cycle).
function DirectedGraph:topological_sort()
local sorting = {}
-- copy self so edges can be removed without affecting the real graph.
local graph, original_vert = graph_copy(self)
-- find vertices without incoming edges.
-- invariant: at the end of each step, sources contains all vertices
-- without incoming edges.
local sources = source_verts(graph)
while #sources > 0 do
-- pop v from sources.
local v = sources[#sources]
sources[#sources] = nil
-- v has no incoming edges, it can be next in the sorting.
-- important!! don't insert v, insert the corresponding vertex from the
-- original graph. The copied vertices are not known outside this
-- function (alternative: maybe return indices in graph.vertices?).
table.insert(sorting, original_vert[v])
-- find vertices which, if v is removed from graph, have no more incoming edges.
-- Those are sources after v is removed.
for outgoing_edge_vert, _ in pairs(v.outgoing_edge_verts) do
-- there is one edge, it has to be from v.
if vim.tbl_count(outgoing_edge_vert.incoming_edge_verts) == 1 then
table.insert(sources, outgoing_edge_vert)
end
end
-- finally: remove v from graph and sources.
graph:clear_vertex(v)
end
if #sorting ~= #self.vertices then
-- error: the sorting does not contain all vertices -> the graph has a cycle.
return nil
end
return sorting
end
-- return all vertices reachable from this one.
function DirectedGraph:connected_component(vert, edge_direction)
local outgoing_vertices_field = edge_direction == "Backward"
and "incoming_edge_verts"
or "outgoing_edge_verts"
local visited = {}
local to_visit = { [vert] = true }
-- get any value in table.
local next_vert, _ = next(to_visit, nil)
while next_vert do
to_visit[next_vert] = nil
visited[next_vert] = true
for neighbor, _ in pairs(next_vert[outgoing_vertices_field]) do
if not visited[neighbor] then
to_visit[neighbor] = true
end
end
next_vert, _ = next(to_visit, nil)
end
return tbl_util.set_to_list(visited)
end
-- Very useful to have a graph where vertices are associated with some label.
-- This just proxies DirectedGraph and swaps labels and vertices in
-- parameters/return-values.
local LabeledDigraph = {}
LabeledDigraph.__index = LabeledDigraph
local function new_labeled_graph()
return setmetatable({
graph = new_graph(),
label_to_vert = {},
vert_to_label = {},
-- map label -> origin-vert -> dest-vert
label_to_verts = autotable(3, { warn = false }),
-- map edge (origin,dest) to set of labels.
verts_to_label = autotable(3, { warn = false }),
}, LabeledDigraph)
end
function LabeledDigraph:set_vertex(label)
if self.label_to_vert[label] then
-- don't add same label again.
return
end
local vert = self.graph:add_vertex()
self.label_to_vert[label] = vert
self.vert_to_label[vert] = label
end
function LabeledDigraph:set_edge(lv1, lv2, edge_label)
-- ensure vertices exist.
self:set_vertex(lv1)
self:set_vertex(lv2)
if self.verts_to_label[lv1][lv2][edge_label] then
-- edge exists, do nothing.
return
end
-- determine before setting the lv1-lv2-edge.
local other_edge_exists = next(self.verts_to_label[lv1][lv2], nil) ~= nil
-- store both associations;
self.verts_to_label[lv1][lv2][edge_label] = true
self.label_to_verts[edge_label][lv1][lv2] = true
if other_edge_exists then
-- there already exists an entry for this edge, no need to add it to
-- the graph.
return
end
self.graph:set_edge(self.label_to_vert[lv1], self.label_to_vert[lv2])
end
function LabeledDigraph:clear_edge(lv1, lv2, ledge)
if not self.verts_to_label[lv1][lv2][ledge] then
-- edge does not exist, do nothing.
return
end
self.verts_to_label[lv1][lv2][ledge] = nil
if next(self.verts_to_label[lv1][lv2]) == nil then
-- removed last edge between v1, v2 -> remove edge in graph.
self.graph:clear_edge(self.label_to_vert[lv1], self.label_to_vert[lv2])
end
end
function LabeledDigraph:clear_edges(label)
for lv1, lv2 in pairs(self.label_to_verts[label]) do
self:clear_edge(lv1, lv2, label)
end
-- set to nil, not {}, so autotable can work its magic.
self.label_to_verts[label] = nil
end
function LabeledDigraph:connected_component(lv, edge_direction)
self:set_vertex(lv)
return vim.tbl_map(function(v)
return self.vert_to_label[v]
end, self.graph:connected_component(self.label_to_vert[lv], edge_direction))
end
return {
new = new_graph,
new_labeled = new_labeled_graph,
}

View File

@ -0,0 +1,188 @@
local builtin_namespace = require("luasnip.util._builtin_vars")
local function tbl_to_lazy_env(tbl)
local function wrapper(varname)
local val_ = tbl[varname]
if type(val_) == "function" then
return val_()
end
return val_
end
return wrapper
end
local namespaces = {}
-- Namespaces allow users to define their own environmet variables
local function _resolve_namespace_var(full_varname)
local parts = vim.split(full_varname, "_")
local nmsp = namespaces[parts[1]]
local varname
-- Is safe to fallback to the buitin-unnamed namespace as the checks in _env_namespace
-- don't allow overriding those vars
if nmsp then
varname = full_varname:sub(#parts[1] + 2)
else
nmsp = namespaces[""]
varname = full_varname
end
return nmsp, varname
end
local Environ = {}
function Environ.is_table(var_fullname)
local nmsp, varname = _resolve_namespace_var(var_fullname)
---@diagnostic disable-next-line: need-check-nil
return nmsp.is_table(varname)
end
function Environ:new(info, o)
o = o or {}
setmetatable(o, self)
vim.list_extend(info, info.pos) -- For compatibility with old user defined namespaces
for ns_name, ns in pairs(namespaces) do
local eager_vars = {}
if ns.init then
eager_vars = ns.init(info)
end
for _, eager in ipairs(ns.eager) do
if not eager_vars[eager] then
eager_vars[eager] = ns.vars(eager)
end
end
local prefix = ""
if ns_name ~= "" then
prefix = ns_name .. "_"
end
for name, val in pairs(eager_vars) do
name = prefix .. name
rawset(o, name, val)
end
end
return o
end
local builtin_ns_names = vim.inspect(vim.tbl_keys(builtin_namespace.builtin_ns))
local function _env_namespace(name, opts)
assert(
opts and type(opts) == "table",
("Your opts for '%s' has to be a table"):format(name)
)
assert(
opts.init or opts.vars,
("Your opts for '%s' needs init or vars"):format(name)
)
-- namespace.eager → ns.vars
assert(
not opts.eager or opts.vars,
("Your opts for %s can't set a `eager` field without the `vars` one"):format(
name
)
)
opts.eager = opts.eager or {}
local multiline_vars = opts.multiline_vars or false
local type_of_it = type(multiline_vars)
assert(
type_of_it == "table"
or type_of_it == "boolean"
or type_of_it == "function",
("Your opts for %s can't have `multiline_vars` of type %s"):format(
name,
type_of_it
)
)
-- If type is function we don't have to override it
if type_of_it == "table" then
local is_table_set = {}
for _, key in ipairs(multiline_vars) do
is_table_set[key] = true
end
opts.is_table = function(key)
return is_table_set[key] or false
end
elseif type_of_it == "boolean" then
opts.is_table = function(_)
return multiline_vars
end
else -- is a function
opts.is_table = multiline_vars
end
if opts.vars and type(opts.vars) == "table" then
opts.vars = tbl_to_lazy_env(opts.vars)
end
namespaces[name] = opts
end
_env_namespace("", builtin_namespace)
-- The exposed api checks for the names to avoid accidental overrides
function Environ.env_namespace(name, opts)
assert(
name:match("^[a-zA-Z][a-zA-Z0-9]*$"),
("You can't create a namespace with name '%s' it has to contain only and at least a non alpha-numeric character"):format(
name
)
)
assert(
not builtin_namespace.builtin_ns[name],
("You can't create a namespace with name '%s' because is one one of %s"):format(
name,
builtin_ns_names
)
)
_env_namespace(name, opts)
end
function Environ:__index(key)
local nmsp, varname = _resolve_namespace_var(key)
---@diagnostic disable-next-line: need-check-nil
local val = nmsp.vars(varname)
rawset(self, key, val)
return val
end
function Environ:override(env, new_env)
for k, v in pairs(new_env) do
env[k] = v
end
end
local fake_env = {
__index = function(tbl, key)
local var
if Environ.is_table(key) then
var = { "$" .. key }
else
var = "$" .. key
end
rawset(tbl, key, var)
return var
end,
}
function Environ.fake()
local o = {}
setmetatable(o, fake_env)
return o
end
return Environ

View File

@ -0,0 +1,18 @@
local node_names = require("luasnip.util.types").names_pascal_case
return {
enter = 1,
leave = 2,
change_choice = 3,
pre_expand = 4,
to_string = function(node_type, event_id)
if event_id == 3 then
return "ChangeChoice"
elseif event_id == 4 then
return "PreExpand"
else
return node_names[node_type]
.. (event_id == 1 and "Enter" or "Leave")
end
end,
}

View File

@ -0,0 +1,159 @@
-- eventually turn ext_opts into proper objects, mainly for
-- default-construction eg. assured `complete`.
--
-- child_*-functions perform the same operation as theiry non-child
-- counterparts, but on a collection (eg.
-- `{[types.insertNode={...}, [types.textNode]= {...}]}`) of ext_opts.
local types = require("luasnip.util.types")
-- vim.tbl_extend always creates a new table, but doesn't accept nil, so we
-- always pass this empty table, which will (has to!) stay empty.
local shared_empty_table = {}
local states = {
"active",
"passive",
"snippet_passive",
"visited",
"unvisited",
}
-- opts: child_ext_opts, have to have hl_group set for all combinations of
-- node-type and active,passive,snippet_passive,visited,unvisited.
local function clear_invalid(opts)
--stylua: ignore start
for _, node_type in pairs(types.node_types) do
for _, state in ipairs(states) do
local state_hl_group = opts[node_type][state].hl_group
opts[node_type][state].hl_group =
vim.fn.hlexists(state_hl_group) == 1 and state_hl_group
or nil
end
end
--stylua: ignore end
end
local function _complete_ext_opts(ext_opts)
if not ext_opts then
ext_opts = {}
end
ext_opts.snippet_passive = ext_opts.snippet_passive or {}
ext_opts.passive = vim.tbl_extend(
"keep",
ext_opts.passive or shared_empty_table,
ext_opts.snippet_passive or shared_empty_table
)
-- both unvisited and visited inherit from passive.
ext_opts.unvisited = vim.tbl_extend(
"keep",
ext_opts.unvisited or shared_empty_table,
ext_opts.passive or shared_empty_table
)
ext_opts.visited = vim.tbl_extend(
"keep",
ext_opts.visited or shared_empty_table,
ext_opts.passive or shared_empty_table
)
-- active inherits from visited.
ext_opts.active = vim.tbl_extend(
"keep",
ext_opts.active or shared_empty_table,
ext_opts.visited or shared_empty_table
)
for _, state in ipairs(states) do
--stylua: ignore start
if ext_opts[state].hl_group and not
ext_opts[state].priority then
ext_opts[state].priority = 0
end
--stylua: ignore end
end
return ext_opts
end
-- active inherits unset values from passive, which in turn inherits from
-- snippet_passive.
-- Also make sure that all keys have a table, and are not nil!
local function child_complete(ext_opts)
for _, node_type in pairs(types.node_types) do
ext_opts[node_type] = _complete_ext_opts(ext_opts[node_type])
end
ext_opts.base_prio = 0
return ext_opts
end
local function complete(ext_opts)
_complete_ext_opts(ext_opts)
ext_opts.base_prio = 0
return ext_opts
end
-- in-place adds opts of b to a, doesn't override.
-- a/b: completed ext_opts, not nil.
local function extend(opts_a, opts_b)
for _, state in ipairs(states) do
opts_a[state] = vim.tbl_extend("keep", opts_a[state], opts_b[state])
end
return opts_a
end
-- in-place adds opts of b to a, doesn't override.
-- a/b: completed child_ext_opts, not nil.
local function child_extend(opts_a, opts_b)
for _, node_type in ipairs(types.node_types) do
extend(opts_a[node_type], opts_b[node_type])
end
return opts_a
end
local function increase_prio(opts, inc)
-- increase only if there is a priority.
for _, state in ipairs(states) do
opts[state].priority = opts[state].priority
and (opts[state].priority + inc)
end
end
-- ext_opts-priorities are defined relative to some base-priority.
-- As nvim_api_buf_set_extmark takes absolute values only, we have to
-- set the absolute priorities, which can vary depending on nesting-level
-- of a given snippet, during runtime, by increasing the relative priorities by
-- either the conf.base_prio or the base-prio used in the previous nesting-level.
local function set_abs_prio(opts, new_base_prio)
-- undo previous increase.
-- base_prio is initialized with 0.
local prio_offset = new_base_prio - opts.base_prio
opts.base_prio = new_base_prio
increase_prio(opts, prio_offset)
return opts
end
local function child_set_abs_prio(opts, new_base_prio)
-- undo previous increase.
-- base_prio is initialized with 0.
local prio_offset = new_base_prio - opts.base_prio
opts.base_prio = new_base_prio
for _, node_type in ipairs(types.node_types) do
increase_prio(opts[node_type], prio_offset)
end
return opts
end
return {
clear_invalid = clear_invalid,
complete = complete,
child_complete = child_complete,
extend = extend,
child_extend = child_extend,
set_abs_prio = set_abs_prio,
child_set_abs_prio = child_set_abs_prio,
}

View File

@ -0,0 +1,82 @@
local M = {}
-- map fn -> {arg_indx = int, extend = fn}[]
local function_properties = setmetatable({}, { __mode = "k" })
local function default_extend(arg, extend)
return vim.tbl_extend("keep", arg or {}, extend or {})
end
---Create a new decorated version of `fn`.
---@param fn The function to create a decorator for.
---@vararg The values to extend with. These should match the descriptions passed
---in `register`:
---```lua
---local function somefn(arg1, arg2, opts1, opts2)
---...
---end
---register(somefn, {arg_indx=4}, {arg_indx=3})
---apply(somefn,
--- {key = "opts2 is extended with this"},
--- {key = "and opts1 with this"})
---```
---@return function: The decorated function.
function M.apply(fn, ...)
local extend_properties = function_properties[fn]
assert(
extend_properties,
"Cannot extend this function, it was not registered! Check :h luasnip-extend_decorator for more infos."
)
local extend_values = { ... }
local decorated_fn = function(...)
local direct_args = { ... }
-- override values of direct argument.
for i, ep in ipairs(extend_properties) do
local arg_indx = ep.arg_indx
-- still allow overriding with directly-passed keys.
direct_args[arg_indx] =
ep.extend(direct_args[arg_indx], extend_values[i])
end
-- important: http://www.lua.org/manual/5.3/manual.html#3.4
-- Passing arguments after the results from `unpack` would mess all this
-- up.
return fn(unpack(direct_args))
end
-- we know how to extend the decorated function!
function_properties[decorated_fn] = extend_properties
return decorated_fn
end
---Prepare a function for usage with extend_decorator.
---To create a decorated function which extends `opts`-style tables passed to it, we need to know
--- 1. which parameter-position the opts are in and
--- 2. how to extend them.
---@param fn function: the function that should be registered.
---@vararg tables. Each describes how to extend one parameter to `fn`.
---The tables accept the following keys:
--- - 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 args passed to the decorated function.
--- It defaults to a function which just extends the the arg-table with the
--- extend-table.
--- This extend-behaviour is adaptable to accomodate `s`, where the first
--- argument may be string or table.
function M.register(fn, ...)
local fn_eps = { ... }
-- make sure ep.extend is set.
for _, ep in ipairs(fn_eps) do
ep.extend = ep.extend or default_extend
end
function_properties[fn] = fn_eps
end
return M

View File

@ -0,0 +1,49 @@
local sn = require("luasnip.nodes.snippet").SN
local t = require("luasnip.nodes.textNode").T
return {
var = function(_, _, node, text)
local v = node.parent.snippet.env[text]
if type(v) == "table" then
-- Avoid issues with empty vars
if #v > 0 then
return v
else
return { "" }
end
else
return { v }
end
end,
better_var = function(varname)
return function(_, parent)
local v = parent.snippet.env[varname]
if type(v) == "table" then
-- Avoid issues with empty vars
if #v > 0 then
return v
else
return { "" }
end
else
return { v }
end
end
end,
eval_vim_dynamic = function(vimstring)
return function()
-- 'echo'd string is returned to lua.
return sn(nil, {
t(
vim.split(
vim.api.nvim_exec("echo " .. vimstring, true),
"\n"
)
),
})
end
end,
copy = function(args)
return args[1]
end,
}

View File

@ -0,0 +1,469 @@
-- Taken from https://github.com/actboy168/json.lua
-- MIT License
--
-- Copyright (c) 2020 actboy168
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
local type = type
local next = next
local error = error
local tonumber = tonumber
local string_char = string.char
local string_byte = string.byte
local string_find = string.find
local string_match = string.match
local string_gsub = string.gsub
local string_sub = string.sub
local string_format = string.format
local utf8_char
if _VERSION == "Lua 5.1" or _VERSION == "Lua 5.2" then
local math_floor = math.floor
function utf8_char(c)
if c <= 0x7f then
return string_char(c)
elseif c <= 0x7ff then
return string_char(math_floor(c / 64) + 192, c % 64 + 128)
elseif c <= 0xffff then
return string_char(
math_floor(c / 4096) + 224,
math_floor(c % 4096 / 64) + 128,
c % 64 + 128
)
elseif c <= 0x10ffff then
return string_char(
math_floor(c / 262144) + 240,
math_floor(c % 262144 / 4096) + 128,
math_floor(c % 4096 / 64) + 128,
c % 64 + 128
)
end
error(string_format("invalid UTF-8 code '%x'", c))
end
else
utf8_char = utf8.char
end
local encode_escape_map = {
['"'] = '\\"',
["\\"] = "\\\\",
["/"] = "\\/",
["\b"] = "\\b",
["\f"] = "\\f",
["\n"] = "\\n",
["\r"] = "\\r",
["\t"] = "\\t",
}
local decode_escape_set = {}
local decode_escape_map = {}
for k, v in next, encode_escape_map do
decode_escape_map[v] = k
decode_escape_set[string_byte(v, 2)] = true
end
local statusBuf
local statusPos
local statusTop
local statusAry = {}
local statusRef = {}
local function find_line()
local line = 1
local pos = 1
while true do
local f, _, nl1, nl2 = string_find(statusBuf, "([\n\r])([\n\r]?)", pos)
if not f then
return line, statusPos - pos + 1
end
local newpos = f + ((nl1 == nl2 or nl2 == "") and 1 or 2)
if newpos > statusPos then
return line, statusPos - pos + 1
end
pos = newpos
line = line + 1
end
end
local function decode_error(msg)
error(string_format("ERROR: %s at line %d col %d", msg, find_line()), 2)
end
local function get_word()
return string_match(statusBuf, "^[^ \t\r\n%]},]*", statusPos)
end
local function skip_comment(b)
if
b ~= 47 --[[ '/' ]]
then
return
end
local c = string_byte(statusBuf, statusPos + 1)
if
c == 42 --[[ '*' ]]
then
-- block comment
local pos = string_find(statusBuf, "*/", statusPos)
if pos then
statusPos = pos + 2
else
statusPos = #statusBuf + 1
end
return true
elseif
c == 47 --[[ '/' ]]
then
-- line comment
local pos = string_find(statusBuf, "[\r\n]", statusPos)
if pos then
statusPos = pos
else
statusPos = #statusBuf + 1
end
return true
end
end
local function next_byte()
local pos = string_find(statusBuf, "[^ \t\r\n]", statusPos)
if pos then
statusPos = pos
local b = string_byte(statusBuf, pos)
if not skip_comment(b) then
return b
end
return next_byte()
end
return -1
end
local function decode_unicode_surrogate(s1, s2)
return utf8_char(
0x10000
+ (tonumber(s1, 16) - 0xd800) * 0x400
+ (tonumber(s2, 16) - 0xdc00)
)
end
local function decode_unicode_escape(s)
return utf8_char(tonumber(s, 16))
end
local function decode_string()
local has_unicode_escape = false
local has_escape = false
local i = statusPos + 1
while true do
i = string_find(statusBuf, '[%z\1-\31\\"]', i)
if not i then
decode_error("expected closing quote for string")
end
local x = string_byte(statusBuf, i)
if x < 32 then
statusPos = i
decode_error("control character in string")
end
if
x == 34 --[[ '"' ]]
then
local s = string_sub(statusBuf, statusPos + 1, i - 1)
if has_unicode_escape then
s = string_gsub(
string_gsub(
s,
"\\u([dD][89aAbB]%x%x)\\u([dD][c-fC-F]%x%x)",
decode_unicode_surrogate
),
"\\u(%x%x%x%x)",
decode_unicode_escape
)
end
if has_escape then
s = string_gsub(s, "\\.", decode_escape_map)
end
statusPos = i + 1
return s
end
--assert(x == 92 --[[ "\\" ]])
local nx = string_byte(statusBuf, i + 1)
if
nx == 117 --[[ "u" ]]
then
if not string_match(statusBuf, "^%x%x%x%x", i + 2) then
statusPos = i
decode_error("invalid unicode escape in string")
end
has_unicode_escape = true
i = i + 6
else
if not decode_escape_set[nx] then
statusPos = i
decode_error(
"invalid escape char '"
.. (nx and string_char(nx) or "<eol>")
.. "' in string"
)
end
has_escape = true
i = i + 2
end
end
end
local function decode_number()
local num, c =
string_match(statusBuf, "^([0-9]+%.?[0-9]*)([eE]?)", statusPos)
if
not num or string_byte(num, -1) == 0x2E --[[ "." ]]
then
decode_error("invalid number '" .. get_word() .. "'")
end
if c ~= "" then
num = string_match(
statusBuf,
"^([^eE]*[eE][-+]?[0-9]+)[ \t\r\n%]},/]",
statusPos
)
if not num then
decode_error("invalid number '" .. get_word() .. "'")
end
end
statusPos = statusPos + #num
return tonumber(num)
end
local function decode_number_zero()
local num, c = string_match(statusBuf, "^(.%.?[0-9]*)([eE]?)", statusPos)
if
not num
or string_byte(num, -1) == 0x2E --[[ "." ]]
or string_match(statusBuf, "^.[0-9]+", statusPos)
then
decode_error("invalid number '" .. get_word() .. "'")
end
if c ~= "" then
num = string_match(
statusBuf,
"^([^eE]*[eE][-+]?[0-9]+)[ \t\r\n%]},/]",
statusPos
)
if not num then
decode_error("invalid number '" .. get_word() .. "'")
end
end
statusPos = statusPos + #num
return tonumber(num)
end
local function decode_number_negative()
statusPos = statusPos + 1
local c = string_byte(statusBuf, statusPos)
if c then
if c == 0x30 then
return -decode_number_zero()
elseif c > 0x30 and c < 0x3A then
return -decode_number()
end
end
decode_error("invalid number '" .. get_word() .. "'")
end
local function decode_true()
if string_sub(statusBuf, statusPos, statusPos + 3) ~= "true" then
decode_error("invalid literal '" .. get_word() .. "'")
end
statusPos = statusPos + 4
return true
end
local function decode_false()
if string_sub(statusBuf, statusPos, statusPos + 4) ~= "false" then
decode_error("invalid literal '" .. get_word() .. "'")
end
statusPos = statusPos + 5
return false
end
local function decode_null()
if string_sub(statusBuf, statusPos, statusPos + 3) ~= "null" then
decode_error("invalid literal '" .. get_word() .. "'")
end
statusPos = statusPos + 4
return vim.NIL
end
local function decode_array()
statusPos = statusPos + 1
local res = {}
local chr = next_byte()
if
chr == 93 --[[ ']' ]]
then
statusPos = statusPos + 1
return res
end
statusTop = statusTop + 1
statusAry[statusTop] = true
statusRef[statusTop] = res
return res
end
local function decode_object()
statusPos = statusPos + 1
local res = {}
local chr = next_byte()
if
chr == 125 --[[ ']' ]]
then
statusPos = statusPos + 1
return vim.empty_dict()
end
statusTop = statusTop + 1
statusAry[statusTop] = false
statusRef[statusTop] = res
return res
end
local decode_uncompleted_map = {
[string_byte('"')] = decode_string,
[string_byte("0")] = decode_number_zero,
[string_byte("1")] = decode_number,
[string_byte("2")] = decode_number,
[string_byte("3")] = decode_number,
[string_byte("4")] = decode_number,
[string_byte("5")] = decode_number,
[string_byte("6")] = decode_number,
[string_byte("7")] = decode_number,
[string_byte("8")] = decode_number,
[string_byte("9")] = decode_number,
[string_byte("-")] = decode_number_negative,
[string_byte("t")] = decode_true,
[string_byte("f")] = decode_false,
[string_byte("n")] = decode_null,
[string_byte("[")] = decode_array,
[string_byte("{")] = decode_object,
}
local function unexpected_character()
decode_error(
"unexpected character '"
.. string_sub(statusBuf, statusPos, statusPos)
.. "'"
)
end
local function unexpected_eol()
decode_error("unexpected character '<eol>'")
end
local decode_map = {}
for i = 0, 255 do
decode_map[i] = decode_uncompleted_map[i] or unexpected_character
end
decode_map[-1] = unexpected_eol
local function decode()
return decode_map[next_byte()]()
end
local function decode_item()
local top = statusTop
local ref = statusRef[top]
if statusAry[top] then
ref[#ref + 1] = decode()
else
local key = decode_string()
if
next_byte() ~= 58 --[[ ':' ]]
then
decode_error("expected ':'")
end
statusPos = statusPos + 1
ref[key] = decode()
end
if top == statusTop then
repeat
local chr = next_byte()
statusPos = statusPos + 1
if
chr == 44 --[[ "," ]]
then
local c = next_byte()
if statusAry[statusTop] then
if
c ~= 93 --[[ "]" ]]
then
return
end
else
if
c ~= 125 --[[ "}" ]]
then
return
end
end
statusPos = statusPos + 1
else
if statusAry[statusTop] then
if
chr ~= 93 --[[ "]" ]]
then
decode_error("expected ']' or ','")
end
else
if
chr ~= 125 --[[ "}" ]]
then
decode_error("expected '}' or ','")
end
end
end
statusTop = statusTop - 1
until statusTop == 0
end
end
local M = {}
function M.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
if str == "" then
error("attempted to decode an empty string")
end
statusBuf = str
statusPos = 1
statusTop = 0
if next_byte() == -1 then
return vim.NIL
end
local res = decode()
while statusTop > 0 do
decode_item()
end
if string_find(statusBuf, "[^ \t\r\n]", statusPos) then
decode_error("trailing garbage")
end
return res
end
return M

View File

@ -0,0 +1,41 @@
local Path = require("luasnip.util.path")
-- neovim-loader does not handle module-names with dots correctly, so for
-- jsregexp-0.0.6, the call to require("jsregexp.core") in jsregexp.lua errors
-- even if the library is in rtp.
-- Resolve path to jsregexp.so manually, and loadlib it in preload (and remove
-- preload after requires are done and have failed/worked).
-- omit "@".
local this_file = debug.getinfo(1).source:sub(2)
local repo_dir = vim.fn.fnamemodify(this_file, ":h:h:h:h")
local jsregexp_core_path = Path.join(repo_dir, "deps", "luasnip-jsregexp.so")
-- rather gracefully, if the path does not exist, or loadlib can't do its job
-- for some other reason, the preload will be set to nil, ie not be set.
--
-- This means we don't hinder a regularly installed 0.0.6-jsregexp-library,
-- since its `require("jsregexp.core")` will be unaffected.
package.preload["jsregexp.core"] =
package.loadlib(jsregexp_core_path, "luaopen_jsregexp_core")
-- jsregexp: first try loading the version installed by luasnip, then global ones.
local jsregexp_ok, jsregexp = pcall(require, "luasnip-jsregexp")
if not jsregexp_ok then
jsregexp_ok, jsregexp = pcall(require, "jsregexp")
end
-- don't want to affect other requires.
package.preload["jsregexp.core"] = nil
if not jsregexp_ok then
return false
end
-- detect version, and return compile-function.
-- 0.0.6-compile_safe and 0.0.5-compile behave the same, ie. nil, err on error.
if jsregexp.compile_safe then
return jsregexp.compile_safe
else
return jsregexp.compile
end

View File

@ -0,0 +1,13 @@
return function(lazy_t, lazy_defs)
return setmetatable(lazy_t, {
__index = function(t, k)
local v = lazy_defs[k]
if v then
local v_resolved = v()
rawset(t, k, v_resolved)
return v_resolved
end
return nil
end,
})
end

View File

@ -0,0 +1,123 @@
local util = require("luasnip.util.util")
-- older neovim-versions (even 0.7.2) do not have stdpath("log").
local logpath_ok, logpath = pcall(vim.fn.stdpath, "log")
if not logpath_ok then
logpath = vim.fn.stdpath("cache")
end
-- just to be sure this dir exists.
-- 448 = 0700
vim.loop.fs_mkdir(logpath, 448)
local log_location = logpath .. "/luasnip.log"
local log_old_location = logpath .. "/luasnip.log.old"
local luasnip_log_fd = vim.loop.fs_open(
log_location,
-- only append.
"a",
-- 420 = 0644
420
)
local function log_line_append(msg)
msg = msg:gsub("\n", "\n | ")
vim.loop.fs_write(luasnip_log_fd, msg .. "\n")
end
if not luasnip_log_fd then
-- print a warning
print(
("LuaSnip: could not open log at %s. Not logging for this session."):format(
log_location
)
)
-- make log_line_append do nothing.
log_line_append = util.nop
else
-- if log_fd found, check if log should be rotated.
local logsize = vim.loop.fs_fstat(luasnip_log_fd).size
if logsize > 10 * 2 ^ 20 then
-- logsize > 10MiB:
-- move log -> old log, start new log.
vim.loop.fs_rename(log_location, log_old_location)
luasnip_log_fd = vim.loop.fs_open(
log_location,
-- only append.
"a",
-- 420 = 0644
420
)
end
end
local M = {}
local log = {
error = function(msg)
log_line_append("ERROR | " .. msg)
end,
warn = function(msg)
log_line_append("WARN | " .. msg)
end,
info = function(msg)
log_line_append("INFO | " .. msg)
end,
debug = function(msg)
log_line_append("DEBUG | " .. msg)
end,
}
-- functions copied directly by deepcopy.
-- will be initialized later on, by set_loglevel.
local effective_log
-- levels sorted by importance, descending.
local loglevels = { "error", "warn", "info", "debug" }
-- special key none disable all logging.
function M.set_loglevel(target_level)
local target_level_indx = util.indx_of(loglevels, target_level)
if target_level == "none" then
target_level_indx = 0
end
assert(target_level_indx ~= nil, "invalid level!")
-- reset effective loglevels, set those with importance higher than
-- target_level, disable (nop) those with lower.
effective_log = {}
for i = 1, target_level_indx do
effective_log[loglevels[i]] = log[loglevels[i]]
end
for i = target_level_indx + 1, #loglevels do
effective_log[loglevels[i]] = util.nop
end
end
function M.new(module_name)
local module_log = {}
for name, _ in pairs(log) do
module_log[name] = function(msg, ...)
-- don't immediately get the referenced function, we'd like to
-- allow changing the loglevel on-the-fly.
effective_log[name](module_name .. ": " .. msg:format(...))
end
end
return module_log
end
function M.open()
vim.cmd(("tabnew %s"):format(log_location))
end
-- to verify log is working.
function M.ping()
log_line_append(("PONG | pong! (%s)"):format(os.date()))
end
-- set default-loglevel.
M.set_loglevel("warn")
return M

View File

@ -0,0 +1,208 @@
local session = require("luasnip.session")
local Mark = {}
function Mark:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
-- opts just like in nvim_buf_set_extmark.
local function mark(pos_begin, pos_end, opts)
return Mark:new({
id = vim.api.nvim_buf_set_extmark(
0,
session.ns_id,
pos_begin[1],
pos_begin[2],
-- override end_* in opts.
vim.tbl_extend(
"force",
opts,
{ end_line = pos_end[1], end_col = pos_end[2] }
)
),
-- store opts here, can't be queried using nvim_buf_get_extmark_by_id.
opts = opts,
})
end
local function bytecol_to_utfcol(pos)
local line = vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)
-- line[1]: get_lines returns table.
-- use utf16-index.
local utf16_indx, _ = vim.str_utfindex(line[1] or "", pos[2])
return { pos[1], utf16_indx }
end
function Mark:pos_begin_end()
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
0,
session.ns_id,
self.id,
{ details = true }
)
return bytecol_to_utfcol({ mark_info[1], mark_info[2] }),
bytecol_to_utfcol({ mark_info[3].end_row, mark_info[3].end_col })
end
function Mark:pos_begin()
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
0,
session.ns_id,
self.id,
{ details = false }
)
return bytecol_to_utfcol({ mark_info[1], mark_info[2] })
end
function Mark:pos_end()
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
0,
session.ns_id,
self.id,
{ details = true }
)
return bytecol_to_utfcol({ mark_info[3].end_row, mark_info[3].end_col })
end
function Mark:pos_begin_end_raw()
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
0,
session.ns_id,
self.id,
{ details = true }
)
return { mark_info[1], mark_info[2] }, {
mark_info[3].end_row,
mark_info[3].end_col,
}
end
function Mark:pos_begin_raw()
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
0,
session.ns_id,
self.id,
{ details = false }
)
return { mark_info[1], mark_info[2] }
end
function Mark:copy_pos_gravs(opts)
local pos_beg, pos_end = self:pos_begin_end_raw()
opts.right_gravity = self.opts.right_gravity
opts.end_right_gravity = self.opts.end_right_gravity
return mark(pos_beg, pos_end, opts)
end
-- opts just like in nvim_buf_set_extmark.
-- opts as first arg bcs. pos are pretty likely to stay the same.
function Mark:update(opts, pos_begin, pos_end)
-- if one is changed, the other is likely as well.
if not pos_begin then
pos_begin = old_pos_begin
if not pos_end then
pos_end = old_pos_end
end
end
-- override with new.
self.opts = vim.tbl_extend("force", self.opts, opts)
vim.api.nvim_buf_set_extmark(
0,
session.ns_id,
pos_begin[1],
pos_begin[2],
vim.tbl_extend(
"force",
self.opts,
{ id = self.id, end_line = pos_end[1], end_col = pos_end[2] }
)
)
end
function Mark:set_opts(opts)
local pos_begin, pos_end = self:pos_begin_end_raw()
vim.api.nvim_buf_del_extmark(0, session.ns_id, self.id)
self.opts = opts
-- set new extmark, current behaviour for updating seems inconsistent,
-- eg. gravs are reset, deco is kept.
self.id = vim.api.nvim_buf_set_extmark(
0,
session.ns_id,
pos_begin[1],
pos_begin[2],
vim.tbl_extend(
"force",
opts,
{ end_line = pos_end[1], end_col = pos_end[2] }
)
)
end
function Mark:set_rgravs(rgrav_left, rgrav_right)
-- don't update if nothing would change.
if
self.opts.right_gravity ~= rgrav_left
or self.opts.end_right_gravity ~= rgrav_right
then
self.opts.right_gravity = rgrav_left
self.opts.end_right_gravity = rgrav_right
self:set_opts(self.opts)
end
end
function Mark:get_rgrav(which)
if which == -1 then
return self.opts.right_gravity
else
return self.opts.end_right_gravity
end
end
function Mark:set_rgrav(which, rgrav)
if which == -1 then
if self.opts.right_gravity == rgrav then
return
end
self.opts.right_gravity = rgrav
else
if self.opts.end_right_gravity == rgrav then
return
end
self.opts.end_right_gravity = rgrav
end
self:set_opts(self.opts)
end
function Mark:get_endpoint(which)
-- simpler for now, look into perf here later.
local l, r = self:pos_begin_end_raw()
if which == -1 then
return l
else
return r
end
end
-- change all opts except rgravs.
function Mark:update_opts(opts)
local opts_cp = vim.deepcopy(opts)
opts_cp.right_gravity = self.opts.right_gravity
opts_cp.end_right_gravity = self.opts.end_right_gravity
self:set_opts(opts_cp)
end
function Mark:clear()
vim.api.nvim_buf_del_extmark(0, session.ns_id, self.id)
end
return {
mark = mark,
}

View File

@ -0,0 +1,350 @@
local ast_utils = require("luasnip.util.parser.ast_utils")
local Ast = require("luasnip.util.parser.neovim_ast")
local tNode = require("luasnip.nodes.textNode")
local iNode = require("luasnip.nodes.insertNode")
local fNode = require("luasnip.nodes.functionNode")
local cNode = require("luasnip.nodes.choiceNode")
local dNode = require("luasnip.nodes.dynamicNode")
local sNode = require("luasnip.nodes.snippet")
local functions = require("luasnip.util.functions")
local Environ = require("luasnip.util.environ")
local session = require("luasnip.session")
local util = require("luasnip.util.util")
local M = {}
local _split = function(s)
return vim.split(s, "\n", { plain = true })
end
local types = ast_utils.types
local to_node
local function fix_node_indices(nodes)
local used_nodes = {}
for _, node in ipairs(nodes) do
if node.pos and node.pos > 0 then
used_nodes[node.pos] = node
end
end
for _, v, i in util.key_sorted_pairs(used_nodes) do
v.pos = i
end
return nodes
end
local function ast2luasnip_nodes(ast_nodes)
local nodes = {}
for i, ast_node in ipairs(ast_nodes) do
nodes[i] = ast_node.parsed
end
return fix_node_indices(nodes)
end
local function var_func(ast)
local varname = ast.name
local transform_func
if ast.transform then
transform_func = ast_utils.apply_transform(ast.transform)
else
transform_func = util.id
end
return function(_, parent, _, variable_default)
local v = parent.snippet.env[varname]
local lines
if type(v) == "table" then
-- Avoid issues with empty vars
if #v > 0 then
lines = v
else
lines = { "" }
end
else
lines = { v }
end
-- quicker than checking `lines` in some way.
if not v then
-- the variable is not defined:
-- insert the variable's name as a placeholder.
return sNode.SN(nil, { iNode.I(1, varname) })
end
if #lines == 0 or (#lines == 1 and #lines[1] == 0) then
-- The variable is empty.
-- default passed as user_arg, rationale described in
-- types.VARIABLE-to_node_func.
if variable_default then
return variable_default
else
-- lines might still just be {} (#lines == 0).
lines = { "" }
end
end
-- v exists and has no default, return the (maybe modified) lines.
return sNode.SN(nil, { tNode.T(transform_func(lines)) })
end
end
local function copy_func(tabstop)
local transform_func
if tabstop.transform then
transform_func = ast_utils.apply_transform(tabstop.transform)
else
transform_func = util.id
end
return function(args)
return transform_func(args[1])
end
end
local function placeholder_func(_, parent, _, placeholder_snip)
local env = parent.snippet.env
-- is_interactive needs env to determine interactiveness.
-- env is passed through to all following is_interactive calls.
if not placeholder_snip:is_interactive(env) then
-- this placeholder only contains text or (transformed)
-- variables, so an insertNode can be generated from its
-- contents.
-- create new snippet that only contains the parsed snippetNode, so we
-- can `fake_expand` and `get_static_text()` it.
local snippet = sNode.S("", placeholder_snip)
-- get active env from snippet.
snippet:fake_expand({ env = env })
local iText = snippet:get_static_text()
-- no need to un-escape iText, that was already done.
return sNode.SN(nil, iNode.I(1, iText))
end
return sNode.SN(
nil,
session.config.parser_nested_assembler(1, placeholder_snip)
)
end
---If this tabstop-node (CHOICE, TABSTOP or PLACEHOLDER) is a copy of another,
---set that up and return, otherwise return false.
---@param ast table: ast-node.
---@return boolean: whether the node is now parsed.
local function tabstop_node_copy_inst(ast)
local existing_tabstop_ast_node = ast.copies
if existing_tabstop_ast_node then
-- this tabstop is a mirror of an already-parsed tabstop/placeholder.
ast.parsed =
fNode.F(copy_func(ast), { existing_tabstop_ast_node.parsed })
return true
end
return false
end
-- these actually create nodes from any AST.
local to_node_funcs = {
-- careful! this parses the snippet into a list of nodes, not a full snippet!
-- The table can then be passed to the regular snippet-constructors.
[types.SNIPPET] = function(ast, _)
ast.parsed = ast2luasnip_nodes(ast.children)
end,
[types.TEXT] = function(ast, _)
local text = _split(ast.esc)
ast.parsed = tNode.T(text)
end,
[types.CHOICE] = function(ast)
-- even choices may be copies.
if tabstop_node_copy_inst(ast) then
return
end
local choices = {}
for i, choice in ipairs(ast.items) do
choices[i] = tNode.T(_split(choice))
end
ast.parsed = cNode.C(ast.tabstop, choices)
end,
[types.TABSTOP] = function(ast)
if tabstop_node_copy_inst(ast) then
return
end
-- tabstops don't have placeholder-text.
ast.parsed = iNode.I(ast.tabstop)
end,
[types.PLACEHOLDER] = function(ast, state)
if tabstop_node_copy_inst(ast) then
return
end
local node
if #ast.children == 1 and ast.children[1].type == types.TEXT then
-- we cannot place a dynamicNode as $0.
-- But all valid ${0:some nodes here} contain just text inside
-- them, so this works :)
node = iNode.I(ast.tabstop, _split(ast.children[1].esc))
else
local snip = sNode.SN(1, ast2luasnip_nodes(ast.children))
node = dNode.D(ast.tabstop, placeholder_func, {}, {
-- pass snip here, again to preserve references to other tables.
user_args = { snip },
})
end
ast.parsed = node
end,
[types.VARIABLE] = function(ast, state)
local var = ast.name
local default
if ast.children then
default = sNode.SN(nil, ast2luasnip_nodes(ast.children))
end
local fn
local is_interactive_fn
if state.var_functions[var] then
fn, is_interactive_fn = unpack(state.var_functions[var])
else
fn = var_func(ast)
-- override the regular `is_interactive` to accurately determine
-- whether the snippet produced by the dynamicNode is interactive
-- or not. This is important when a variable is wrapped inside a
-- placeholder: ${1:$TM_SELECTED_TEXT}
-- With variable-environments we cannot tell at parse-time whether
-- the dynamicNode will be just text, an insertNode or some other
-- nodes(the default), so that has to happen at runtime now.
is_interactive_fn = function(_, env)
local var_value = env[var]
if not var_value then
-- inserts insertNode.
return true
end
-- just wrap it for more uniformity.
if type(var_value) == "string" then
var_value = { var_value }
end
if
(#var_value == 1 and #var_value[1] == 0)
or #var_value == 0
then
-- var is empty, default is inserted.
-- if no default, it's not interactive (an empty string is inserted).
return default and default:is_interactive()
end
-- variable is just inserted, not interactive.
return false
end
end
local d = dNode.D(ast.potential_tabstop, fn, {}, {
-- TRICKY!!!!
-- Problem: if the default is passed to the dynamicNode-function via lambda-capture, the
-- copy-routine, which will run on expansion, cannot associate these
-- nodes inside the passed nodes with the ones that are inside the
-- snippet.
-- For example, if `default` contains a functionNode which relies on
-- an insertNode within the snippet, it has the insertNode as an
-- argnode stored inside it. During copy, the copied insertNode (eg
-- a pointer to it) has to be inserted at this position as well,
-- otherwise there might be bugs (the snippet thinks the argnode is
-- present, but it isn't).
--
-- This means that these nodes may not be passed as a simple
-- lambda-capture (!!).
-- I don't really like this, it can lead to very subtle errors (not
-- in this instance, but needing to do this in general).
--
-- TODO: think about ways to avoid this. OTOH, this is almost okay,
-- just needs to be documented a bit.
--
-- `default` is potentially nil.
user_args = { default },
})
d.is_interactive = is_interactive_fn
-- if the variable is preceded by \n<indent>, the indent is applied to
-- all lines of the variable (important for eg. TM_SELECTED_TEXT).
if ast.previous_text ~= nil and #ast.previous_text > 1 then
local last_line_indent =
ast.previous_text[#ast.previous_text]:match("^%s+$")
if last_line_indent then
-- TM_SELECTED_TEXT contains the indent of the selected
-- snippets, which leads to correct indentation if the
-- snippet is expanded at the position the text was removed
-- from.
-- This seems pretty stupid, but TM_SELECTED_TEXT is
-- desigend to be compatible with vscode.
-- Use SELECT_DEDENT insted.
-- stylua: ignore
local indentstring = var ~= "TM_SELECTED_TEXT"
and "$PARENT_INDENT" .. last_line_indent
or last_line_indent
-- adjust current d's jump-position..
d.pos = 1
-- ..so it has the correct position when wrapped inside a
-- snippetNode.
d = sNode.ISN(ast.potential_tabstop, { d }, indentstring)
end
end
ast.parsed = d
end,
}
--- Converts any ast into luasnip-nodes.
--- Snippets return a table of nodes, those can be used like the return-value of `fmt`.
---@param ast table: AST, as generated by `require("vim.lsp._snippet").parse`
---@param state table:
--- - `var_functions`: table, string -> {dNode-fn, is_interactive_fn}
--- For now, only used when parsing snipmate-snippets.
---@return table: node corresponding to `ast`.
function to_node(ast, state)
if not Ast.is_node(ast) then
-- ast is not an ast (probably a luasnip-node), return it as-is.
return ast
end
return to_node_funcs[ast.type](ast, state)
end
--- Converts any ast into usable nodes.
---@param ast table: AST, as generated by `require("vim.lsp._snippet").parse`
---@param state table:
--- - `var_functions`: table, string -> {dNode-fn, is_interactive_fn}
--- For now, only used when parsing snipmate-snippets.
---@return table: list of luasnip-nodes.
function M.to_luasnip_nodes(ast, state)
state = state or {}
state.var_functions = state.var_functions or {}
ast_utils.give_vars_potential_tabstop(ast)
-- fix disallowed $0 in snippet.
-- TODO(logging): report changes here.
ast_utils.fix_zero(ast)
-- Variables need the text just in front of them to determine whether to
-- indent all lines of the Variable.
ast_utils.give_vars_previous_text(ast)
local ast_nodes_topsort = ast_utils.parse_order(ast)
assert(
ast_nodes_topsort,
"cannot represent snippet: contains circular dependencies"
)
for _, node in ipairs(ast_nodes_topsort) do
to_node(node, state)
end
return ast.parsed
end
return M

View File

@ -0,0 +1,461 @@
local Ast = require("luasnip.util.parser.neovim_ast")
local types = Ast.node_type
local util = require("luasnip.util.util")
local Str = require("luasnip.util.str")
local log = require("luasnip.util.log").new("parser")
local jsregexp_compile_safe = require("luasnip.util.jsregexp")
local directed_graph = require("luasnip.util.directed_graph")
local M = {}
---Walks ast pre-order, from left to right, applying predicate fn.
---The walk is aborted as soon as fn matches (eg. returns true).
---The walk does not recurse into Transform or choice, eg. it only covers nodes
---that can be jumped (in)to.
---@param ast table: the tree.
---@param fn function: the predicate.
---@return boolean: whether the predicate matched.
local function predicate_ltr_nodes(ast, fn)
if fn(ast) then
return true
end
for _, node in ipairs(ast.children or {}) do
if predicate_ltr_nodes(node, fn) then
return true
end
end
return false
end
-- tested in vscode:
-- in "${1|b,c|} ${1:aa}" ${1:aa} is the copy,
-- in "${1:aa}, ${1|b,c|}" ${1|b,c} is the copy => with these two the position
-- determines which is the real tabstop => they have the same priority.
-- in "$1 ${1:aa}", $1 is the copy, so it has to have a lower priority.
local function type_real_tabstop_prio(node)
local _type_real_tabstop_prio = {
[types.TABSTOP] = 1,
[types.PLACEHOLDER] = 2,
[types.CHOICE] = 2,
}
if node.transform then
return 0
end
return _type_real_tabstop_prio[node.type]
end
---The name of this function is horrible, but I can't come up with something
---more succinct.
---The idea here is to find which of two nodes is "smaller" in a
---"real-tabstop"-ordering relation on all the nodes of a snippet.
---REQUIREMENT!!! The nodes have to be passed in the order they appear in in
---the snippet, eg. prev_node has to appear earlier in the text (or be a parent
---of) current_node.
---@param prev_node table: the ast node earlier in the text.
---@param current_node table: the other ast node.
---@return boolean: true if prev_node is less than (according to the
---"real-tabstop"-ordering described above and in the docstring of
---`add_dependents`), false otherwise.
local function real_tabstop_order_less(prev_node, current_node)
local prio_prev = type_real_tabstop_prio(prev_node)
local prio_current = type_real_tabstop_prio(current_node)
-- if type-prio is the same, the one that appeared earlier is the real tabstop.
return prio_prev == prio_current and false or prio_prev < prio_current
end
---Find the real (eg. the one that is not a copy) $0.
---@param ast table: ast
---@return number, number, boolean: first, the type of the node with position 0, then
--- the child of `ast` containing it and last whether the real $0 is copied.
local function real_zero_node(ast)
local real_zero = nil
local real_zero_indx = nil
local is_copied = false
local _search_zero
_search_zero = function(node)
local had_zero = false
-- find placeholder/tabstop/choice with position 0
if node.tabstop == 0 then
if not real_zero then
real_zero = node
had_zero = true
else
if real_tabstop_order_less(real_zero, node) then
-- node has a higher prio than the current real_zero.
real_zero = node
had_zero = true
end
-- we already encountered a zero-node, since i(0) cannot be
-- copied this has to be reported to the caller.
is_copied = true
end
end
for indx, child in ipairs(node.children or {}) do
local zn, _ = _search_zero(child)
-- due to recursion, this will be called last in the loop of the
-- outermost snippet.
-- real_zero_indx will be the position of the child of snippet, in
-- which the real $0 is located.
if zn then
real_zero_indx = indx
had_zero = true
end
end
return had_zero
end
_search_zero(ast)
return real_zero, real_zero_indx, is_copied
end
local function count_tabstop(ast, tabstop_indx)
local count = 0
predicate_ltr_nodes(ast, function(node)
if node.tabstop == tabstop_indx then
count = count + 1
end
-- only stop once all nodes were looked at.
return false
end)
return count
end
local function text_only_placeholder(placeholder)
local only_text = true
predicate_ltr_nodes(placeholder, function(node)
if node == placeholder then
-- ignore placeholder.
return false
end
if node.type ~= types.TEXT then
only_text = false
-- we found non-text, no need to search more.
return true
end
end)
return only_text
end
local function max_position(ast)
local max = 0
predicate_ltr_nodes(ast, function(node)
local new_max = node.tabstop or 0
if new_max > max then
max = new_max
end
-- don't stop early.
return false
end)
return max
end
local function replace_position(ast, p1, p2)
predicate_ltr_nodes(ast, function(node)
if node.tabstop == p1 then
node.tabstop = p2
end
-- look at all nodes.
return false
end)
end
function M.fix_zero(ast)
local zn, ast_child_with_0_indx, is_copied = real_zero_node(ast)
-- if zn exists, is a tabstop, an immediate child of `ast`, and does not
-- have to be copied, the snippet can be accurately represented by luasnip.
-- (also if zn just does not exist, ofc).
--
-- If the snippet can't be represented as-is, the ast needs to be modified
-- as described below.
if
not zn
or (
zn
and not is_copied
and (zn.type == types.TABSTOP or (zn.type == types.PLACEHOLDER and text_only_placeholder(
zn
)))
and ast.children[ast_child_with_0_indx] == zn
)
then
return
end
-- bad, a choice or placeholder is at position 0.
-- replace all ${0:...} with ${n+1:...} (n highest position)
-- max_position is at least 0, all's good.
local max_pos = max_position(ast)
replace_position(ast, 0, max_pos + 1)
-- insert $0 as a direct child to snippet, just behind the original $0/the
-- node containing it.
table.insert(ast.children, ast_child_with_0_indx + 1, Ast.tabstop(0))
end
---This function identifies which tabstops/placeholder/choices are copies, and
---which are "real tabstops"(/choices/placeholders). The real tabstops are
---extended with a list of their dependents (tabstop.dependents), the copies
---with their real tabstop (copy.copies)
---
---Rules for which node of any two nodes with the same tabstop-index is the
---real tabstop:
--- - if one is a tabstop and the other a placeholder/choice, the
--- placeholder/choice is the real tabstop.
--- - if they are both tabstop or both placeholder/choice, the one which
--- appears earlier in the snippet is the real tabstop.
--- (in "${1: ${1:lel}}" the outer ${1:...} appears earlier).
---
---@param ast table: the AST.
function M.add_dependents(ast)
-- all nodes that have a tabstop.
-- map tabstop-index (number) -> node.
local tabstops = {}
-- nodes which copy some tabstop.
-- map tabstop-index (number) -> node[] (since there could be multiple copies of that one snippet).
local copies = {}
predicate_ltr_nodes(ast, function(node)
if not node.tabstop then
-- not a tabstop-node -> continue.
return false
end
if not tabstops[node.tabstop] then
tabstops[node.tabstop] = node
-- continue, we want to find all dependencies.
return false
end
if not copies[node.tabstop] then
copies[node.tabstop] = {}
end
if real_tabstop_order_less(tabstops[node.tabstop], node) then
table.insert(copies[node.tabstop], tabstops[node.tabstop])
tabstops[node.tabstop] = node
else
table.insert(copies[node.tabstop], node)
end
-- continue.
return false
end)
-- associate real tabstop with its copies (by storing the copies in the real tabstop).
for i, real_tabstop in pairs(tabstops) do
real_tabstop.dependents = {}
for _, copy in ipairs(copies[i] or {}) do
table.insert(real_tabstop.dependents, copy)
copy.copies = real_tabstop
end
end
end
local function apply_modifier(text, modifier)
local mod_fn = Str.vscode_string_modifiers[modifier]
if mod_fn then
return mod_fn(text)
else
-- this can't really be reached, since only correct and available
-- modifiers are parsed successfully
-- (https://github.com/L3MON4D3/LuaSnip/blob/5fbebf6409f86bc4b7b699c2c80745e1ed190c16/lua/luasnip/util/parser/neovim_parser.lua#L239-L245).
log.warn(
"Tried to apply unknown modifier `%s` while parsing snippet, recovering by applying identity instead.",
modifier
)
return text
end
end
local function apply_transform_format(nodes, captures)
local transformed = ""
for _, node in ipairs(nodes) do
if node.type == types.TEXT then
transformed = transformed .. node.esc
else
local capture = captures[node.capture_index]
-- capture exists if it ..exists.. and is nonempty.
if capture and #capture > 0 then
if node.if_text then
transformed = transformed .. node.if_text
elseif node.modifier then
transformed = transformed
.. apply_modifier(capture, node.modifier)
else
transformed = transformed .. capture
end
else
if node.else_text then
transformed = transformed .. node.else_text
end
end
end
end
return transformed
end
function M.apply_transform(transform)
if jsregexp_compile_safe then
local reg_compiled, err =
jsregexp_compile_safe(transform.pattern, transform.option)
if reg_compiled then
-- can be passed to functionNode!
return function(lines)
-- luasnip expects+passes lines as list, but regex needs one string.
lines = table.concat(lines, "\n")
local matches = reg_compiled(lines)
local transformed = ""
-- index one past the end of previous match.
-- This is used to append unmatched characters to `transformed`, so
-- it's initialized such that the first append is from 1.
local prev_match_end = 0
for _, match in ipairs(matches) do
-- begin_ind and end_ind are inclusive.
transformed = transformed
.. lines:sub(prev_match_end + 1, match.begin_ind - 1)
.. apply_transform_format(
transform.format,
match.groups
)
-- end-inclusive
prev_match_end = match.end_ind
end
transformed = transformed
.. lines:sub(prev_match_end + 1, #lines)
return vim.split(transformed, "\n")
end
else
log.error(
"Failed parsing regex `%s` with options `%s`: %s",
transform.pattern,
transform.option,
err
)
-- fall through to returning identity.
end
end
-- without jsregexp, or without a valid regex, we cannot properly transform
-- whatever is supposed to be transformed here.
-- Just return a function that returns the to-be-transformed string
-- unmodified.
return util.id
end
---Variables need the text which is in front of them to determine whether they
---have to be indented ("asdf\n\t$TM_SELECTED_TEXT": vscode indents all lines
---of TM_SELECTED_TEXT).
---
---The text is accessible as ast_node.previous_text, a string[].
---@param ast table: the AST.
function M.give_vars_previous_text(ast)
local last_text = { "" }
-- important: predicate_ltr_nodes visits the node in the order they appear,
-- textually, in the snippet.
-- This is necessary to actually ensure the variables actually get the text just in front of them.
predicate_ltr_nodes(ast, function(node)
if node.children then
-- continue if this node is not a leaf.
-- Since predicate_ltr_nodes runs fn first for the placeholder, and
-- then for its' children, `last_text` would be reset wrongfully
-- (example: "asdf\n\t${1:$TM_SELECTED_TEXT}". Here the placeholder
-- is encountered before the variable -> no indentation).
--
-- ignoring non-leaf-nodes makes it so that only the nodes which
-- actually contribute text (placeholders are "invisible" in that
-- they don't add text themselves, they do it through their
-- children) are considered.
return false
end
if node.type == types.TEXT then
last_text = vim.split(node.esc, "\n")
elseif node.type == types.VARIABLE then
node.previous_text = last_text
else
-- reset last_text when a different node is encountered.
last_text = { "" }
end
-- continue..
return false
end)
end
---Variables are turned into placeholders if the Variable is undefined or not set.
---Since in luasnip, variables can be added at runtime, the decision whether a
---variable is just some text, inserts its default, or its variable-name has to
---be deferred to runtime.
---So, each variable is a dynamicNode, and needs a tabstop.
---In vscode the variables are visited
--- 1) after all other tabstops/placeholders/choices and
--- 2) in the order they appear in the snippet-body.
---We mimic this behaviour.
---@param ast table: The AST.
function M.give_vars_potential_tabstop(ast)
local last_tabstop = max_position(ast)
predicate_ltr_nodes(ast, function(node)
if node.type == types.VARIABLE then
last_tabstop = last_tabstop + 1
node.potential_tabstop = last_tabstop
end
end)
end
function M.parse_order(ast)
M.add_dependents(ast)
-- build Directed Graph from ast-nodes.
-- vertices are ast-nodes, edges define has-to-be-parsed-before-relations
-- (a child of some placeholder would have an edge to it, real tabstops
-- have edges to their copies).
local g = directed_graph.new()
-- map node -> vertex.
local to_vert = {}
-- add one vertex for each node + create map node->vert.
predicate_ltr_nodes(ast, function(node)
to_vert[node] = g:add_vertex()
end)
predicate_ltr_nodes(ast, function(node)
if node.dependents then
-- if the node has dependents, it has to be parsed before they are.
for _, dep in ipairs(node.dependents) do
g:set_edge(to_vert[node], to_vert[dep])
end
end
if node.children then
-- if the node has children, they have to be parsed before it can
-- be parsed.
for _, child in ipairs(node.children) do
g:set_edge(to_vert[child], to_vert[node])
end
end
end)
local topsort = g:topological_sort()
if not topsort then
-- ast (with additional dependencies) contains circle.
return nil
end
local to_node = util.reverse_lookup(to_vert)
return vim.tbl_map(function(vertex)
return to_node[vertex]
end, topsort)
end
M.types = types
return M

View File

@ -0,0 +1,141 @@
local sNode = require("luasnip.nodes.snippet")
local ast_parser = require("luasnip.util.parser.ast_parser")
local parse = require("luasnip.util.parser.neovim_parser").parse
local Ast = require("luasnip.util.parser.neovim_ast")
local Str = require("luasnip.util.str")
local functions = require("luasnip.util.functions")
local util = require("luasnip.util.util")
local extend_decorator = require("luasnip.util.extend_decorator")
local M = {}
---Parse snippet represented by `body`.
---@param context (table|string|number|nil):
--- - table|string: treated like the first argument to `ls.snippet`,
--- returns a snippet.
--- - number: Returns a snippetNode, `context` is its' jump-position.
--- - nil: Returns a flat list of luasnip-nodes, to be used however.
---@param body string: the representation of the snippet.
---@param opts table|nil: optional parameters. Valid keys:
--- - `trim_empty`: boolean, remove empty lines from the snippet.
--- - `dedent`: boolean, remove common indent from the snippet's lines.
--- - `variables`: map[string-> (fn()->string)], variables to be used only in this
--- snippet.
---@return table: the snippet, in the representation dictated by the value of
---`context`.
function M.parse_snippet(context, body, opts)
opts = opts or {}
if opts.dedent == nil then
opts.dedent = true
end
if opts.trim_empty == nil then
opts.trim_empty = true
end
body = Str.sanitize(body)
local lines = vim.split(body, "\n")
Str.process_multiline(lines, opts)
body = table.concat(lines, "\n")
local ast
if body == "" then
ast = Ast.snippet({
Ast.text(""),
})
else
ast = parse(body)
end
local nodes = ast_parser.to_luasnip_nodes(ast, {
var_functions = opts.variables,
})
if type(context) == "number" then
return sNode.SN(context, nodes)
end
if type(context) == "nil" then
return nodes
end
if type(context) == "string" then
context = { trig = context }
end
context.docstring = body
return sNode.S(context, nodes)
end
local function context_extend(arg, extend)
local argtype = type(arg)
if argtype == "string" then
arg = { trig = arg }
end
if argtype == "table" then
return vim.tbl_extend("keep", arg, extend or {})
end
-- fall back to unchanged arg.
-- log this, probably.
return arg
end
extend_decorator.register(
M.parse_snippet,
{ arg_indx = 1, extend = context_extend },
{ arg_indx = 3 }
)
local function backticks_to_variable(body)
local var_map = {}
local variable_indx = 1
local var_string = ""
local processed_to = 1
for from, to in Str.unescaped_pairs(body, "`", "`") do
local varname = "LUASNIP_SNIPMATE_VAR" .. variable_indx
var_string = var_string
-- since the first unescaped ` is at from, there is no unescaped `
-- in body:sub(old_to, from-1). We can therefore gsub occurences of
-- \`, without worrying about potentially changing something like
-- \\` (or \\\\`) into \` (\\\`).
.. body:sub(processed_to, from - 1):gsub("\\`", "`")
-- `$varname` is unsafe, might lead to something like "my
-- snip$LUASNIP_SNIPMATE_VAR1pet", where the variable is
-- interpreted as "LUASNIP_SNIPMATE_VAR1pet".
-- This cannot happen with curly braces.
.. "${"
.. varname
.. "}"
-- don't include backticks in vimscript.
var_map[varname] =
functions.eval_vim_dynamic(body:sub(from + 1, to - 1))
processed_to = to + 1
variable_indx = variable_indx + 1
end
-- append remaining characters.
var_string = var_string .. body:sub(processed_to, -1):gsub("\\`", "`")
return var_map, var_string
end
function M.parse_snipmate(context, body, opts)
local new_vars
new_vars, body = backticks_to_variable(body)
opts = opts or {}
opts.variables = {}
for name, fn in pairs(new_vars) do
-- created dynamicNode is not interactive.
opts.variables[name] = { fn, util.no }
end
return M.parse_snippet(context, body, opts)
end
extend_decorator.register(
M.parse_snipmate,
{ arg_indx = 1, extend = context_extend },
{ arg_indx = 3 }
)
return M

View File

@ -0,0 +1,207 @@
-- ripped out of neovim.
local M = {}
local node_type = {
SNIPPET = 0,
TABSTOP = 1,
PLACEHOLDER = 2,
VARIABLE = 3,
CHOICE = 4,
TRANSFORM = 5,
FORMAT = 6,
TEXT = 7,
}
M.node_type = node_type
local Node = {}
function Node:__tostring()
local insert_text = {}
if self.type == node_type.SNIPPET then
for _, c in ipairs(self.children) do
table.insert(insert_text, tostring(c))
end
elseif self.type == node_type.CHOICE then
table.insert(insert_text, self.items[1])
elseif self.type == node_type.PLACEHOLDER then
for _, c in ipairs(self.children or {}) do
table.insert(insert_text, tostring(c))
end
elseif self.type == node_type.TEXT then
table.insert(insert_text, self.esc)
end
return table.concat(insert_text, "")
end
--- @private
local function new(t)
return setmetatable(t, Node)
end
---Determine whether {t} is an AST-node.
---@param t table
---@return boolean
local function is_node(t)
return getmetatable(t) == Node
end
M.is_node = is_node
---Create a new snippet.
---@param children (ast-node[]) Contents of the snippet.
---@return table |lsp-parser-snippet|
function M.snippet(children)
return new({
type = node_type.SNIPPET,
children = children,
})
end
---Create a new tabstop.
---@param tabstop (number) Position of this tabstop.
---@param transform (table|nil) optional transform applied to the tabstop.
---@return table |lsp-parser-tabstop|
function M.tabstop(tabstop, transform)
return new({
type = node_type.TABSTOP,
tabstop = tabstop,
transform = transform,
})
end
---Create a new placeholder.
---@param tabstop (number) Position of the placeholder.
---@param children (ast-node[]) Content of the placeholder.
---@return table |lsp-parser-placeholder|
function M.placeholder(tabstop, children)
return new({
type = node_type.PLACEHOLDER,
tabstop = tabstop,
children = children,
})
end
---Create a new variable.
---@param name (string) Name.
---@param replacement (node[] | transform | nil)
--- - (node[]) Inserted when the variable is empty.
--- - (transform) Applied to the variable's value.
---@return table |lsp-parser-variable|
function M.variable(name, replacement)
local transform, children
-- transform is an ast-node, children a flat list of nodes.
if is_node(replacement) then
transform = replacement
else
children = replacement
end
return new({
type = node_type.VARIABLE,
name = name,
transform = transform,
children = children,
})
end
---Create a new choice.
---@param tabstop (number) Position of the choice.
---@param items (string[]) Choices.
---@return table |lsp-parser-choice|
function M.choice(tabstop, items)
return new({
type = node_type.CHOICE,
tabstop = tabstop,
items = items,
})
end
---Create a new transform.
---@param pattern (string) Regex applied to the variable/tabstop this transform
--- is supplied to.
---@param format (table of Format|Text) Replacement for the regex.
---@param option (string|nil) Regex-options, default "".
---@return table |lsp-parser-transform|
function M.transform(pattern, format, option)
return new({
type = node_type.TRANSFORM,
pattern = pattern,
format = format,
option = option or "",
})
end
---Create a new format which either inserts the capture at {capture_index},
---applies a modifier to the capture or inserts {if_text} if the capture is
---nonempty, and {else_text} otherwise.
---@param capture_index (number) Capture this format is applied to.
---@param capture_transform (string | table | nil)
--- - (string): {capture_transform} is a modifier.
--- - (table): {capture_transform} can contain either of
--- - {if_text} (string) Inserted for nonempty
--- capture.
--- - {else_text} (string) Inserted for empty or
--- undefined capture.
---@return table |lsp-parser-format|
function M.format(capture_index, capture_transform)
local if_text, else_text, modifier
if type(capture_transform) == "table" then
if_text = capture_transform.if_text
else_text = capture_transform.else_text
elseif type(capture_transform) == "string" then
modifier = capture_transform
end
return new({
type = node_type.FORMAT,
capture_index = capture_index,
modifier = modifier,
if_text = if_text,
else_text = else_text,
})
end
---Create new text.
---@param esc (string) Escaped text.
---@param raw (string|nil, default {esc}) Unescaped text.
---
---@return table |lsp-parser-text|
function M.text(esc, raw)
return new({
type = node_type.TEXT,
esc = esc,
raw = raw or esc,
})
end
function M.merge_adjacent_text(ast)
if ast.children then
-- build new table of children.
local new_children = {}
-- last_child shall always point to the last entry in new_children.
local last_child
for _, child in ipairs(ast.children) do
-- first, recurse into children.
-- When we do this is not important, since it does not change the TEXT-nodes, here is just comfortable.
M.merge_adjacent_text(child)
if
child.type == node_type.TEXT
and last_child
and last_child.type == node_type.TEXT
then
last_child.raw = last_child.raw .. child.raw
last_child.esc = last_child.esc .. child.esc
else
table.insert(new_children, child)
last_child = child
end
end
ast.children = new_children
end
end
return M

View File

@ -0,0 +1,479 @@
-- ripped out of neovim.
local P = {}
local ast = require("luasnip.util.parser.neovim_ast")
---Take characters until the target characters (The escape sequence is '\' + char)
---@param targets string[] The character list for stop consuming text.
---@param specials string[] If the character isn't contained in targets/specials, '\' will be left.
---@private
function P.take_until(targets, specials)
targets = targets or {}
specials = specials or {}
return function(input, pos)
local new_pos = pos
local raw = {}
local esc = {}
while new_pos <= #input do
local c = string.sub(input, new_pos, new_pos)
if c == "\\" then
table.insert(raw, "\\")
new_pos = new_pos + 1
c = string.sub(input, new_pos, new_pos)
if
not vim.tbl_contains(targets, c)
and not vim.tbl_contains(specials, c)
then
table.insert(esc, "\\")
end
table.insert(raw, c)
table.insert(esc, c)
new_pos = new_pos + 1
else
if vim.tbl_contains(targets, c) then
break
end
table.insert(raw, c)
table.insert(esc, c)
new_pos = new_pos + 1
end
end
if new_pos == pos then
return P.unmatch(pos)
end
return {
parsed = true,
value = {
raw = table.concat(raw, ""),
esc = table.concat(esc, ""),
},
pos = new_pos,
}
end
end
---@private
function P.unmatch(pos)
return {
parsed = false,
value = nil,
pos = pos,
}
end
---@private
function P.map(parser, map)
return function(input, pos)
local result = parser(input, pos)
if result.parsed then
return {
parsed = true,
value = map(result.value),
pos = result.pos,
}
end
return P.unmatch(pos)
end
end
---@private
function P.lazy(factory)
return function(input, pos)
return factory()(input, pos)
end
end
---@private
function P.token(token)
return function(input, pos)
local maybe_token = string.sub(input, pos, pos + #token - 1)
if token == maybe_token then
return {
parsed = true,
value = maybe_token,
pos = pos + #token,
}
end
return P.unmatch(pos)
end
end
---@private
function P.pattern(p)
return function(input, pos)
local maybe_match = string.match(string.sub(input, pos), "^" .. p)
if maybe_match then
return {
parsed = true,
value = maybe_match,
pos = pos + #maybe_match,
}
end
return P.unmatch(pos)
end
end
---@private
function P.many(parser)
return function(input, pos)
local values = {}
local new_pos = pos
while new_pos <= #input do
local result = parser(input, new_pos)
if not result.parsed then
break
end
table.insert(values, result.value)
new_pos = result.pos
end
if #values > 0 then
return {
parsed = true,
value = values,
pos = new_pos,
}
end
return P.unmatch(pos)
end
end
---@private
function P.any(...)
local parsers = { ... }
return function(input, pos)
for _, parser in ipairs(parsers) do
local result = parser(input, pos)
if result.parsed then
return result
end
end
return P.unmatch(pos)
end
end
---@private
function P.opt(parser)
return function(input, pos)
local result = parser(input, pos)
return {
parsed = true,
value = result.value,
pos = result.pos,
}
end
end
---@private
function P.seq(...)
local parsers = { ... }
return function(input, pos)
local values = {}
local new_pos = pos
for i, parser in ipairs(parsers) do
local result = parser(input, new_pos)
if result.parsed then
values[i] = result.value
new_pos = result.pos
else
return P.unmatch(pos)
end
end
return {
parsed = true,
value = values,
pos = new_pos,
}
end
end
---see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
local S = {}
S.dollar = P.token("$")
S.open = P.token("{")
S.close = P.token("}")
S.colon = P.token(":")
S.slash = P.token("/")
S.comma = P.token(",")
S.pipe = P.token("|")
S.plus = P.token("+")
S.minus = P.token("-")
S.question = P.token("?")
S.int = P.map(P.pattern("[0-9]+"), function(value)
return tonumber(value, 10)
end)
S.var = P.pattern("[%a_][%w_]+")
S.text = function(targets, specials)
return P.map(P.take_until(targets, specials), function(value)
return ast.text(value.esc, value.raw)
end)
end
S.patterntext = function(pattern)
return P.map(P.pattern(pattern), function(value)
return ast.text(value, value)
end)
end
S.text_or_empty = function(targets, specials)
return P.map(
P.any(P.take_until(targets, specials), P.token("")),
function(value)
-- if it is empty, we have to return a valid S.text-object.
if value == "" then
return ast.text("", "")
else
return ast.text(value.esc, value.raw)
end
end
)
end
S.toplevel = P.lazy(function()
return P.any(S.placeholder, S.tabstop, S.variable, S.choice)
end)
S.format = P.any(
P.map(P.seq(S.dollar, S.int), function(values)
return ast.format(values[2])
end),
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
return ast.format(values[3])
end),
P.map(
P.seq(
S.dollar,
S.open,
S.int,
S.colon,
S.slash,
P.any(
P.token("upcase"),
P.token("downcase"),
P.token("capitalize"),
P.token("camelcase"),
P.token("pascalcase")
),
S.close
),
function(values)
return ast.format(values[3], values[6])
end
),
P.map(
P.seq(
S.dollar,
S.open,
S.int,
S.colon,
P.seq(
S.question,
P.opt(P.take_until({ ":" }, { "\\" })),
S.colon,
P.opt(P.take_until({ "}" }, { "\\" }))
),
S.close
),
function(values)
return ast.format(values[3], {
if_text = values[5][2] and values[5][2].esc or "",
else_text = values[5][4] and values[5][4].esc or "",
})
end
),
P.map(
P.seq(
S.dollar,
S.open,
S.int,
S.colon,
P.seq(S.plus, P.opt(P.take_until({ "}" }, { "\\" }))),
S.close
),
function(values)
return ast.format(values[3], {
if_text = values[5][2] and values[5][2].esc or "",
})
end
),
P.map(
P.seq(
S.dollar,
S.open,
S.int,
S.colon,
S.minus,
P.opt(P.take_until({ "}" }, { "\\" })),
S.close
),
function(values)
return ast.format(values[3], {
else_text = values[6] and values[6].esc or "",
})
end
),
P.map(
P.seq(
S.dollar,
S.open,
S.int,
S.colon,
P.opt(P.take_until({ "}" }, { "\\" })),
S.close
),
function(values)
return ast.format(values[3], {
else_text = values[5] and values[5].esc or "",
})
end
)
)
S.transform = P.map(
P.seq(
S.slash,
P.take_until({ "/" }, { "\\" }),
S.slash,
P.many(
P.any(
S.format,
S.text({ "$", "/" }, { "\\" }),
S.patterntext("[^/]")
)
),
S.slash,
P.opt(P.pattern("[ig]+"))
),
function(values)
return ast.transform(values[2].raw, values[4], values[6])
end
)
S.tabstop = P.any(
P.map(P.seq(S.dollar, S.int), function(values)
return ast.tabstop(values[2])
end),
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
return ast.tabstop(values[3])
end),
P.map(P.seq(S.dollar, S.open, S.int, S.transform, S.close), function(values)
return ast.tabstop(values[3], values[4])
end)
)
S.placeholder = P.any(
P.map(
P.seq(
S.dollar,
S.open,
S.int,
S.colon,
P.opt(
P.many(
P.any(
S.toplevel,
S.text({ "$", "}" }, { "\\" }),
S.patterntext("[^}]")
)
)
),
S.close
),
function(values)
-- no children -> manually create empty text.
return ast.placeholder(values[3], values[5] or { ast.text("") })
end
)
)
S.choice = P.map(
P.seq(
S.dollar,
S.open,
S.int,
S.pipe,
P.opt(
P.many(
P.map(
P.seq(S.text_or_empty({ ",", "|" }), S.comma),
function(values)
return values[1].esc
end
)
)
),
P.map(P.any(S.text_or_empty({ ",", "|" }), S.empty), function(values)
return values.esc
end),
S.pipe,
S.close
),
function(values)
local choices = values[5] or {}
table.insert(choices, values[6])
return ast.choice(values[3], choices)
end
)
S.variable = P.any(
P.map(P.seq(S.dollar, S.var), function(values)
return ast.variable(values[2])
end),
P.map(P.seq(S.dollar, S.open, S.var, S.close), function(values)
return ast.variable(values[3])
end),
P.map(P.seq(S.dollar, S.open, S.var, S.transform, S.close), function(values)
return ast.variable(values[3], values[4])
end),
P.map(
P.seq(
S.dollar,
S.open,
S.var,
S.colon,
P.many(
P.any(
S.toplevel,
S.text({ "$", "}" }, { "\\" }),
S.patterntext("[^}]")
)
),
S.close
),
function(values)
return ast.variable(values[3], values[5])
end
)
)
S.snippet = P.map(
P.many(
P.any(S.toplevel, S.text({ "$" }, { "}", "\\" }), S.patterntext("."))
),
function(values)
-- Insp(values)
return ast.snippet(values)
end
)
local M = {}
---Build the AST for {input}.
---@param input string A snippet as defined in
--- https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
---@return (Snippet)
function M.parse(input)
local result = S.snippet(input, 1)
if not result.parsed then
error("snippet parsing failed.")
end
ast.merge_adjacent_text(result.value)
return result.value
end
M.ast = ast
return M

View File

@ -0,0 +1,266 @@
local Path = {}
local uv = vim.loop
local sep = (function()
if jit then
local os = jit.os:lower()
if vim.tbl_contains({ "linux", "osx", "bsd" }, os) then
return "/"
else
return "\\"
end
end
return package.config:sub(1, 1)
end)()
local root_pattern = (function()
return uv.os_uname().sysname:find("Windows") and "%w%:" or "%/"
end)()
function Path.join(...)
return table.concat({ ... }, sep)
end
function Path.exists(filepath)
return uv.fs_stat(filepath) and true or false
end
function Path.async_read_file(path, callback)
uv.fs_open(path, "r", tonumber("0666", 8), function(err, fd)
assert(not err, err)
uv.fs_fstat(fd, function(err, stat)
assert(not err, err)
uv.fs_read(fd, stat.size, 0, function(err, buffer)
assert(not err, err)
uv.fs_close(fd, function(err)
assert(not err, err)
callback(buffer)
end)
end)
end)
end)
end
---@param path string
---@return string buffer @content of file
function Path.read_file(path)
-- permissions: rrr
local fd = assert(uv.fs_open(path, "r", tonumber("0444", 8)))
local stat = assert(uv.fs_fstat(fd))
-- read from offset 0.
local buf = assert(uv.fs_read(fd, stat.size, 0))
uv.fs_close(fd)
return buf
end
local MYCONFIG_ROOT
if vim.env.MYVIMRC then
MYCONFIG_ROOT = vim.fn.fnamemodify(vim.env.MYVIMRC, ":p:h")
else
MYCONFIG_ROOT = vim.fn.getcwd()
end
-- sometimes we don't want to resolve symlinks, but handle ~/ and ./
function Path.expand_keep_symlink(filepath)
-- omit second return-value of :gsub
local res = filepath
:gsub("^~", vim.env.HOME)
:gsub("^[.][/\\]", MYCONFIG_ROOT .. sep)
return res
end
function Path.expand(filepath)
return uv.fs_realpath(Path.expand_keep_symlink(filepath))
end
-- do our best at normalizing a non-existing path.
function Path.normalize_nonexisting(filepath, cwd)
cwd = cwd or vim.fn.getcwd()
local normalized = filepath
-- replace multiple slashes by one.
:gsub(sep .. sep .. "+", sep)
-- remove trailing slash.
:gsub(sep .. "$", "")
-- remove ./ from path.
:gsub("%." .. sep, "")
-- if not yet absolute, prepend path to current directory.
if not normalized:match("^" .. root_pattern .. "") then
normalized = Path.join(cwd, normalized)
end
return normalized
end
function Path.expand_nonexisting(filepath, cwd)
filepath
-- replace ~ with home-directory.
:gsub("^~", vim.env.HOME)
-- replace ./ or .\ with config-directory (likely ~/.config/nvim)
:gsub(
"^[.][/\\]",
MYCONFIG_ROOT .. sep
)
return Path.normalize_nonexisting(filepath, cwd)
end
-- do our best at expanding a path that may or may not exist (ie. check if it
-- exists, if so do regular expand, and guess expanded path otherwise)
-- Not the clearest name :/
function Path.expand_maybe_nonexisting(filepath, cwd)
local real_expanded = Path.expand(filepath)
if not real_expanded then
real_expanded = Path.expand_nonexisting(filepath, cwd)
end
return real_expanded
end
function Path.normalize_maybe_nonexisting(filepath, cwd)
local real_normalized = Path.normalize(filepath)
if not real_normalized then
real_normalized = Path.normalize_nonexisting(filepath, cwd)
end
return real_normalized
end
---Return files and directories in path as a list
---@param root string
---@return string[] files, string[] directories
function Path.scandir(root)
local files, dirs = {}, {}
local fs = uv.fs_scandir(root)
if fs then
local name, type = "", ""
while name do
name, type = uv.fs_scandir_next(fs)
local path = Path.join(root, name)
-- On networked filesystems, it can happen that we get
-- a name, but no type. In this case, we must query the
-- type manually via fs_stat(). See issue:
-- https://github.com/luvit/luv/issues/660
if name and not type then
local stat = uv.fs_stat(path)
type = stat and stat.type
end
if type == "file" then
table.insert(files, path)
elseif type == "directory" then
table.insert(dirs, path)
elseif type == "link" then
local followed_path = uv.fs_realpath(path)
if followed_path then
local stat = uv.fs_stat(followed_path)
if stat.type == "file" then
table.insert(files, path)
elseif stat.type == "directory" then
table.insert(dirs, path)
end
end
end
end
end
return files, dirs
end
---Get basename
---@param filepath string
---@param ext boolean if true, separate the file extension
---@return string, string?
---Example:
--- Path.basename("~/.config/nvim/init.lua") -> init.lua
--- Path.basename("~/.config/nvim/init.lua", true) -> init, lua
function Path.basename(filepath, ext)
local base = filepath
if base:find(sep) then
base = base:match(("%s([^%s]+)$"):format(sep, sep))
end
if ext then
return base:match("(.*)%.(.+)")
else
return base
end
end
function Path.extension(fname)
return fname:match("%.([^%.]+)$")
end
function Path.components(path)
return vim.split(path, sep, { plain = true, trimempty = true })
end
---Get parent of a path, without trailing separator
---if path is a directory or does not have a parent, returns nil
---Example:
--- On platforms that use "\\" backslash as path separator, e.g., Windows:
--- Path.parent("C:/project_root/file.txt") -- returns "C:/project_root"
--- Path.parent([[C:\project_root\file.txt]]) -- returns [[C:\project_root]]
---
--- -- the followings return `nil`s
--- Path.parent("C:/")
--- Path.parent([[C:\]])
--- Path.parent([[C:\project_root\]])
---
--- -- WARN: although it's unlikely that we will reach the driver's root
--- -- level, Path.parent("C:\file.txt") returns "C:", and please be
--- -- cautious when passing the parent path to some vim functions because
--- -- some vim functions on Windows treat "C:" as a file instead:
--- -- vim.fn.fnamemodify("C:", ":p") -- returns $CWD .. sep .. "C:"
--- -- To get the desired result, use vim.fn.fnamemodify("C:" .. sep, ":p")
---
--- On platforms that use "/" forward slash as path separator, e.g., linux:
--- Path.parent("/project_root/file.txt") returns "/project_root"
--- Path.parent("/file.txt") returns ""
---
--- -- the followings return `nil`s
--- Path.parent("/")
--- Path.parent("/project_root/")
---
--- -- backslash in a valid filename character in linux:
--- Path.parent([[/project_root/\valid\file\name.txt]]) returns "/project_root"
Path.parent = (function()
---@alias PathSeparator "/" | "\\"
---@param os_sep PathSeparator
---@return fun(string): string | nil
local function generate_parent(os_sep)
if os_sep == "/" then
---@param path string
---@return string | nil
return function(path)
local last_component = path:match("[/]+[^/]+$")
if not last_component then
return nil
end
return path:sub(1, #path - #last_component)
end
else
---@param path string
---@return string | nil
return function(path)
local last_component = path:match("[/\\]+[^/\\]+$")
if not last_component then
return nil
end
return path:sub(1, #path - #last_component)
end
end
end
-- for test only
if __LUASNIP_TEST_SEP_OVERRIDE then
return generate_parent
else
return generate_parent(sep)
end
end)()
-- returns nil if the file does not exist!
Path.normalize = uv.fs_realpath
return Path

View File

@ -0,0 +1,137 @@
local is_class = {
a = true,
c = true,
d = true,
l = true,
p = true,
s = true,
u = true,
w = true,
x = true,
z = true,
-- and uppercase versions.
A = true,
C = true,
D = true,
L = true,
P = true,
S = true,
U = true,
W = true,
X = true,
Z = true,
-- all others false.
}
local is_rep_mod = {
["+"] = true,
["*"] = true,
["-"] = true,
["?"] = true,
}
local function is_escaped(text, indx)
local count = 0
for i = indx - 1, 1, -1 do
if string.sub(text, i, i) == "%" then
count = count + 1
else
break
end
end
return count % 2 == 1
end
local function charset_end_indx(string, start_indx)
-- set plain
local indx = string:find("]", start_indx, true)
-- find unescaped ']'
while indx and is_escaped(string, indx) do
indx = string:find("]", indx + 1, true)
end
return indx
end
return {
tokenize = function(pattern)
local indx = 1
local current_text = ""
local tokens = {}
-- assume the pattern starts with text (as opposed to eg. a character
-- class), worst-case an empty textNode is (unnecessarily) inserted at
-- the beginning.
local is_text = true
while indx <= #pattern do
local next_indx
local next_text
local next_is_text
-- for some atoms *,+,-,? are not applicable, ignore them.
local repeatable = true
local char = pattern:sub(indx, indx)
if char == "%" then
if pattern:sub(indx + 1, indx + 1) == "b" then
-- %b seems to consume exactly the next two chars literally.
next_is_text = false
next_indx = indx + 4
repeatable = false
elseif is_class[pattern:sub(indx + 1, indx + 1)] then
next_is_text = false
next_indx = indx + 2
else
-- not a class, just an escaped character.
next_is_text = true
next_indx = indx + 2
-- only append escaped char, not '%'.
end
elseif char == "." then
next_is_text = false
next_indx = indx + 1
elseif char == "[" then
next_is_text = false
-- if not found, just exit loop now, pattern is malformed.
next_indx = (charset_end_indx(pattern, indx) or #pattern) + 1
elseif
char == "("
or char == ")"
or (char == "^" and indx == 1)
then
-- ^ is interpreted literally if not at beginning.
-- $ will always be interpreted literally in triggers.
-- remove ( and ) from text.
-- keep text or no-text active.
next_is_text = is_text
-- increase indx to exclude ( from tokens.
indx = indx + 1
next_indx = indx
-- cannot repeat group.
repeatable = false
else
next_is_text = true
next_indx = indx + 1
end
if repeatable and is_rep_mod[pattern:sub(next_indx, next_indx)] then
next_indx = next_indx + 1
next_is_text = false
end
next_text = pattern:sub(indx, next_indx - 1)
-- check if this token is still the same as the previous.
if next_is_text == is_text then
current_text = current_text .. next_text
else
tokens[#tokens + 1] = current_text
current_text = next_text
end
indx = next_indx
is_text = next_is_text
end
-- add last part, would normally be added at the end of the loop.
tokens[#tokens + 1] = current_text
return tokens
end,
}

View File

@ -0,0 +1,141 @@
local M = {}
local SELECT_RAW = "LUASNIP_SELECT_RAW"
local SELECT_DEDENT = "LUASNIP_SELECT_DEDENT"
local TM_SELECT = "LUASNIP_TM_SELECT"
function M.retrieve()
local ok, val = pcall(vim.api.nvim_buf_get_var, 0, SELECT_RAW)
if ok then
local result = {
val,
vim.api.nvim_buf_get_var(0, SELECT_DEDENT),
vim.api.nvim_buf_get_var(0, TM_SELECT),
}
vim.api.nvim_buf_del_var(0, SELECT_RAW)
vim.api.nvim_buf_del_var(0, SELECT_DEDENT)
vim.api.nvim_buf_del_var(0, TM_SELECT)
return unpack(result)
end
return {}, {}, {}
end
local function get_min_indent(lines)
-- "^(%s*)%S": match only lines that actually contain text.
local min_indent = lines[1]:match("^(%s*)%S")
for i = 2, #lines do
-- %s* -> at least matches
local line_indent = lines[i]:match("^(%s*)%S")
-- ignore if not matched.
if line_indent then
-- if no line until now matched, use line_indent.
if not min_indent or #line_indent < #min_indent then
min_indent = line_indent
end
end
end
return min_indent
end
local function store_registers(...)
local names = { ... }
local restore_data = {}
for _, name in ipairs(names) do
restore_data[name] = {
data = vim.fn.getreg(name),
type = vim.fn.getregtype(name),
}
end
return restore_data
end
local function restore_registers(restore_data)
for name, name_restore_data in pairs(restore_data) do
vim.fn.setreg(name, name_restore_data.data, name_restore_data.type)
end
end
-- subtle: `:lua` exits VISUAL, which means that the '< '>-marks will be set correctly!
-- Afterwards, we can just use <cmd>lua, which does not change the mode.
M.select_keys =
[[:lua require("luasnip.util.select").pre_cut()<Cr>gv"zs<cmd>lua require('luasnip.util.select').post_cut("z")<Cr>]]
local saved_registers
local lines
local start_line, start_col, end_line, end_col
local mode
function M.pre_cut()
-- store registers so we don't change any of them.
-- "" is affected since we perform a cut (s), 1-9 also (although :h
-- quote_number seems to state otherwise for cuts to specific registers..?).
saved_registers =
store_registers("", "1", "2", "3", "4", "5", "6", "7", "8", "9", "z")
-- store data needed for de-indenting lines.
start_line = vim.fn.line("'<") - 1
start_col = vim.fn.col("'<")
end_line = vim.fn.line("'>") - 1
end_col = vim.fn.col("'>")
-- +1: include final line.
lines = vim.api.nvim_buf_get_lines(0, start_line, end_line + 1, true)
mode = vim.fn.visualmode()
end
function M.post_cut(register_name)
-- remove trailing newline.
local chunks = vim.split(vim.fn.getreg(register_name):gsub("\n$", ""), "\n")
-- make sure to restore the registers to the state they were before cutting.
restore_registers(saved_registers)
local tm_select, select_dedent = vim.deepcopy(chunks), vim.deepcopy(chunks)
local min_indent = get_min_indent(lines) or ""
if mode == "V" then
tm_select[1] = tm_select[1]:gsub("^%s+", "")
-- remove indent from all lines:
for i = 1, #select_dedent do
select_dedent[i] = select_dedent[i]:gsub("^" .. min_indent, "")
end
-- due to the trailing newline of the last line, and vim.split's
-- behaviour, the last line of `chunks` is always empty.
-- Keep this
elseif mode == "v" then
-- if selection starts inside indent, remove indent.
if #min_indent > start_col then
select_dedent[1] = lines[1]:gsub(min_indent, "")
end
for i = 2, #select_dedent - 1 do
select_dedent[i] = select_dedent[i]:gsub(min_indent, "")
end
-- remove as much indent from the last line as possible.
if #min_indent > end_col then
select_dedent[#select_dedent] = ""
else
select_dedent[#select_dedent] =
select_dedent[#select_dedent]:gsub("^" .. min_indent, "")
end
else
-- in block: if indent is in block, remove the part of it that is inside
-- it for select_dedent.
if #min_indent > start_col then
local indent_in_block = min_indent:sub(start_col, #min_indent)
for i, line in ipairs(chunks) do
select_dedent[i] = line:gsub("^" .. indent_in_block, "")
end
end
end
vim.api.nvim_buf_set_var(0, SELECT_RAW, chunks)
vim.api.nvim_buf_set_var(0, SELECT_DEDENT, select_dedent)
vim.api.nvim_buf_set_var(0, TM_SELECT, tm_select)
lines = nil
start_line, start_col, end_line, end_col = nil, nil, nil, nil
mode = nil
end
return M

View File

@ -0,0 +1,141 @@
-- Some string processing utility functions
local M = {}
---In-place dedents strings in lines.
---@param lines string[].
local function dedent(lines)
if #lines > 0 then
local ind_size = math.huge
for i, _ in ipairs(lines) do
local i1, i2 = lines[i]:find("^%s*[^%s]")
if i1 and i2 < ind_size then
ind_size = i2
end
end
for i, _ in ipairs(lines) do
lines[i] = lines[i]:sub(ind_size, -1)
end
end
end
---Applies opts to lines.
---lines is modified in-place.
---@param lines string[].
---@param options table, required, can have values:
--- - trim_empty: removes empty first and last lines.
--- - dedent: removes indent common to all lines.
function M.process_multiline(lines, options)
if options.trim_empty then
if lines[1]:match("^%s*$") then
table.remove(lines, 1)
end
if #lines > 0 and lines[#lines]:match("^%s*$") then
lines[#lines] = nil
end
end
if options.dedent then
dedent(lines)
end
end
function M.dedent(s)
local lst = vim.split(s, "\n")
dedent(lst)
return table.concat(lst, "\n")
end
local function is_escaped(s, indx)
local count = 0
for i = indx - 1, 1, -1 do
if string.sub(s, i, i) == "\\" then
count = count + 1
else
break
end
end
return count % 2 == 1
end
--- return position of next (relative to `start`) unescaped occurence of
--- `target` in `s`.
---@param s string
---@param target string
---@param start number
local function find_next_unescaped(s, target, start)
while true do
local from = s:find(target, start, true)
if not from then
return nil
end
if not is_escaped(s, from) then
return from
end
start = from + 1
end
end
--- Creates iterator that returns all positions of substrings <left>.*<right>
--- in `s`, where left and right are not escaped.
--- Only complete pairs left,right are returned, an unclosed left is ignored.
---@param s string
---@param left string
---@param right string
---@return function: iterator, returns pairs from,to.
function M.unescaped_pairs(s, left, right)
local search_from = 1
return function()
local match_from = find_next_unescaped(s, left, search_from)
if not match_from then
return nil
end
local match_to = find_next_unescaped(s, right, match_from + 1)
if not match_to then
return nil
end
search_from = match_to + 1
return match_from, match_to
end
end
function M.aupatescape(s)
if vim.fn.has("win32") or vim.fn.has("win64") then
-- windows: replace \ with / for au-pattern.
s, _ = s:gsub("\\", "/")
end
local escaped, _ = s:gsub(",", "\\,")
return vim.fn.fnameescape(escaped)
end
function M.sanitize(str)
return str:gsub("%\r", "")
end
-- string-operations implemented according to
-- https://github.com/microsoft/vscode/blob/71c221c532996c9976405f62bb888283c0cf6545/src/vs/editor/contrib/snippet/browser/snippetParser.ts#L372-L415
-- such that they can be used for snippet-transformations in vscode-snippets.
local function capitalize(str)
-- uppercase first character.
return str:gsub("^.", string.upper)
end
local function pascalcase(str)
local pascalcased = ""
for match in str:gmatch("[a-zA-Z0-9]+") do
pascalcased = pascalcased .. capitalize(match)
end
return pascalcased
end
M.vscode_string_modifiers = {
upcase = string.upper,
downcase = string.lower,
capitalize = capitalize,
pascalcase = pascalcase,
camelcase = function(str)
-- same as pascalcase, but first character lowercased.
return pascalcase(str):gsub("^.", string.lower)
end,
}
return M

View File

@ -0,0 +1,39 @@
---Convert set of values to a list of those values.
---@generic T
---@param tbl T|T[]|table<T, boolean>
---@return table<T, boolean>
local function set_to_list(tbl)
local ls = {}
for v, _ in pairs(tbl) do
table.insert(ls, v)
end
return ls
end
---Convert value or list of values to a table of booleans for fast lookup.
---@generic T
---@param values T|T[]|table<T, boolean>
---@return table<T, boolean>
local function list_to_set(values)
if values == nil then
return {}
end
if type(values) ~= "table" then
return { [values] = true }
end
local list = {}
for _, v in ipairs(values) do
list[v] = true
end
return list
end
return {
list_to_set = list_to_set,
set_to_list = set_to_list,
}

View File

@ -0,0 +1,13 @@
-- http://lua-users.org/wiki/TimeZone
local function get_timezone_offset(ts)
local utcdate = os.date("!*t", ts)
local localdate = os.date("*t", ts)
localdate.isdst = false -- this is the trick
local diff = os.difftime(os.time(localdate), os.time(utcdate))
local h, m = math.modf(diff / 3600)
return string.format("%+.4d", 100 * h + 60 * m)
end
return {
get_timezone_offset = get_timezone_offset,
}

View File

@ -0,0 +1,34 @@
return {
textNode = 1,
insertNode = 2,
functionNode = 3,
snippetNode = 4,
choiceNode = 5,
dynamicNode = 6,
snippet = 7,
exitNode = 8,
restoreNode = 9,
node_types = { 1, 2, 3, 4, 5, 6, 7, 8, 9 },
names = {
"textNode",
"insertNode",
"functionNode",
"snippetNode",
"choiceNode",
"dynamicNode",
"snippet",
"exitNode",
"restoreNode",
},
names_pascal_case = {
"TextNode",
"InsertNode",
"FunctionNode",
"SnippetNode",
"ChoiceNode",
"DynamicNode",
"Snippet",
"ExitNode",
"RestoreNode",
},
}

View File

@ -0,0 +1,522 @@
local session = require("luasnip.session")
local function get_cursor_0ind()
local c = vim.api.nvim_win_get_cursor(0)
c[1] = c[1] - 1
return c
end
-- don't use utf-indexed column, win_set_cursor ignores these.
local function set_cursor_0ind(c)
c[1] = c[1] + 1
vim.api.nvim_win_set_cursor(0, c)
end
-- pos: (0,0)-indexed.
local function line_chars_before(pos)
-- cur-rows are 1-indexed, api-rows 0.
local line = vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)
return string.sub(line[1], 1, pos[2])
end
-- returns current line with text up-to and excluding the cursor.
local function get_current_line_to_cursor()
return line_chars_before(get_cursor_0ind())
end
-- delete n chars before cursor, MOVES CURSOR
local function remove_n_before_cur(n)
local cur = get_cursor_0ind()
vim.api.nvim_buf_set_text(0, cur[1], cur[2] - n, cur[1], cur[2], { "" })
cur[2] = cur[2] - n
set_cursor_0ind(cur)
end
-- in-place modifies the table.
local function dedent(text, indentstring)
-- 2 because 1 shouldn't contain indent.
for i = 2, #text do
text[i] = text[i]:gsub("^" .. indentstring, "")
end
return text
end
-- in-place insert indenstrig before each line.
local function indent(text, indentstring)
for i = 2, #text - 1, 1 do
-- only indent if there is actually text.
if #text[i] > 0 then
text[i] = indentstring .. text[i]
end
end
-- assuming that the last line should be indented as it is probably
-- followed by some other node, therefore isn't an empty line.
if #text > 1 then
text[#text] = indentstring .. text[#text]
end
return text
end
--- In-place expands tabs in `text`.
--- Difficulties:
--- we cannot simply replace tabs with a given number of spaces, the tabs align
--- text at multiples of `tabwidth`. This is also the reason we need the number
--- of columns the text is already indented by (otherwise we can only start a 0).
---@param text string[], multiline string.
---@param tabwidth number, displaycolumns one tab should shift following text
--- by.
---@param parent_indent_displaycolumns number, displaycolumn this text is
--- already at.
---@return string[], `text` (only for simple nesting).
local function expand_tabs(text, tabwidth, parent_indent_displaycolumns)
for i, line in ipairs(text) do
local new_line = ""
local start_indx = 1
while true do
local tab_indx = line:find("\t", start_indx, true)
-- if no tab found, sub till end (ie. -1).
new_line = new_line .. line:sub(start_indx, (tab_indx or 0) - 1)
if tab_indx then
-- #new_line is index of this tab in new_line.
new_line = new_line
.. string.rep(
" ",
tabwidth
- (
(parent_indent_displaycolumns + #new_line)
% tabwidth
)
)
else
-- reached end of string.
break
end
start_indx = tab_indx + 1
end
text[i] = new_line
end
return text
end
local function tab_width()
return vim.bo.shiftwidth ~= 0 and vim.bo.shiftwidth or vim.bo.tabstop
end
local function mark_pos_equal(m1, m2)
local p1 = vim.api.nvim_buf_get_extmark_by_id(0, session.ns_id, m1, {})
local p2 = vim.api.nvim_buf_get_extmark_by_id(0, session.ns_id, m2, {})
return p1[1] == p2[1] and p1[2] == p2[2]
end
local function move_to_mark(id)
local new_cur_pos
new_cur_pos = vim.api.nvim_buf_get_extmark_by_id(
0,
session.ns_id,
id,
{ details = false }
)
set_cursor_0ind(new_cur_pos)
end
local function bytecol_to_utfcol(pos)
local line = vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)
-- line[1]: get_lines returns table.
return { pos[1], vim.str_utfindex(line[1] or "", pos[2]) }
end
local function replace_feedkeys(keys, opts)
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes(keys, true, false, true),
-- folds are opened manually now, no need to pass t.
-- n prevents langmap from interfering.
opts or "n",
true
)
end
-- pos: (0,0)-indexed.
local function cursor_set_keys(pos, before)
if before then
if pos[2] == 0 then
pos[1] = pos[1] - 1
-- pos2 is set to last columnt of previous line.
-- # counts bytes, but win_set_cursor expects bytes, so all's good.
pos[2] =
#vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1]
else
pos[2] = pos[2] - 1
end
end
return "<cmd>lua vim.api.nvim_win_set_cursor(0,{"
-- +1, win_set_cursor starts at 1.
.. pos[1] + 1
.. ","
-- -1 works for multibyte because of rounding, apparently.
.. pos[2]
.. "})"
.. "<cr><cmd>:silent! foldopen!<cr>"
end
-- any for any mode.
-- other functions prefixed with eg. normal have to be in that mode, the
-- initial esc removes that need.
local function any_select(b, e)
-- stylua: ignore
replace_feedkeys(
-- this esc -> movement sometimes leads to a slight flicker
-- TODO: look into preventing that reliably.
-- Go into visual, then place endpoints.
-- This is to allow us to place the cursor on the \n of a line.
-- see #1158
"<esc>"
-- open folds that contain this selection.
-- we assume that the selection is contained in at most one fold, and
-- that that fold covers b.
-- if we open the fold while visual is active, the selection will be
-- wrong, so this is necessary before we enter VISUAL.
.. cursor_set_keys(b)
-- start visual highlight and move to b again.
-- since we are now in visual, this might actually move the cursor.
.. "v"
.. cursor_set_keys(b)
-- swap to other end of selection, and move it to e.
.. "o"
.. (vim.o.selection == "exclusive" and
cursor_set_keys(e) or
-- set before
cursor_set_keys(e, true))
.. "o<C-G><C-r>_" )
end
local function normal_move_on_insert(new_cur_pos)
-- moving in normal and going into insert is kind of annoying, eg. when the
-- cursor is, in normal, on a tab, i will set it on the beginning of the
-- tab. There's more problems, but this is very safe.
replace_feedkeys("i" .. cursor_set_keys(new_cur_pos))
end
local function insert_move_on(new_cur_pos)
-- maybe feedkeys this too.
set_cursor_0ind(new_cur_pos)
vim.api.nvim_command("redraw!")
end
local function multiline_equal(t1, t2)
for i, line in ipairs(t1) do
if line ~= t2[i] then
return false
end
end
return #t1 == #t2
end
local function word_under_cursor(cur, line)
local ind_start = 1
local ind_end = #line
while true do
local tmp = string.find(line, "%W%w", ind_start)
if not tmp then
break
end
if tmp > cur[2] + 1 then
break
end
ind_start = tmp + 1
end
local tmp = string.find(line, "%w%W", cur[2] + 1)
if tmp then
ind_end = tmp
end
return string.sub(line, ind_start, ind_end)
end
-- Put text and update cursor(pos) where cursor is byte-indexed.
local function put(text, pos)
vim.api.nvim_buf_set_text(0, pos[1], pos[2], pos[1], pos[2], text)
-- add rows
pos[1] = pos[1] + #text - 1
-- add columns, start at 0 if no rows were added, else at old col-value.
pos[2] = (#text > 1 and 0 or pos[2]) + #text[#text]
end
--[[ Wraps the value in a table if it's not one, makes
the first element an empty str if the table is empty]]
local function to_string_table(value)
if not value then
return { "" }
end
if type(value) == "string" then
return { value }
end
-- at this point it's a table
if #value == 0 then
return { "" }
end
-- non empty table
return value
end
-- Wrap node in a table if it is not one
local function wrap_nodes(nodes)
-- safe to assume, if nodes has a metatable, it is a single node, not a
-- table.
if getmetatable(nodes) and nodes.type then
return { nodes }
else
return nodes
end
end
local function pos_equal(p1, p2)
return p1[1] == p2[1] and p1[2] == p2[2]
end
local function string_wrap(lines, pos)
local new_lines = vim.deepcopy(lines)
if #new_lines == 1 and #new_lines[1] == 0 then
return { "$" .. (pos and tostring(pos) or "{}") }
end
new_lines[1] = "${"
.. (pos and (tostring(pos) .. ":") or "")
.. new_lines[1]
new_lines[#new_lines] = new_lines[#new_lines] .. "}"
return new_lines
end
-- Heuristic to extract the comment style from the commentstring
local _comments_cache = {}
local function buffer_comment_chars()
local commentstring = vim.bo.commentstring
if _comments_cache[commentstring] then
return _comments_cache[commentstring]
end
local comments = { "//", "/*", "*/" }
local placeholder = "%s"
local index_placeholder = commentstring:find(vim.pesc(placeholder))
if index_placeholder then
index_placeholder = index_placeholder - 1
if index_placeholder + #placeholder == #commentstring then
comments[1] = vim.trim(commentstring:sub(1, -#placeholder - 1))
else
comments[2] = vim.trim(commentstring:sub(1, index_placeholder))
comments[3] = vim.trim(
commentstring:sub(index_placeholder + #placeholder + 1, -1)
)
end
end
_comments_cache[commentstring] = comments
return comments
end
local function to_line_table(table_or_string)
local tbl = to_string_table(table_or_string)
-- split entries at \n.
local line_table = {}
for _, str in ipairs(tbl) do
local split = vim.split(str, "\n", true)
for i = 1, #split do
line_table[#line_table + 1] = split[i]
end
end
return line_table
end
local function find_outer_snippet(node)
while node.parent do
node = node.parent
end
return node
end
local function redirect_filetypes(fts)
local snippet_fts = {}
for _, ft in ipairs(fts) do
vim.list_extend(snippet_fts, session.ft_redirect[ft])
end
return snippet_fts
end
local function deduplicate(list)
vim.validate({ list = { list, "table" } })
local ret = {}
local contains = {}
for _, v in ipairs(list) do
if not contains[v] then
table.insert(ret, v)
contains[v] = true
end
end
return ret
end
local function get_snippet_filetypes()
local config = require("luasnip.session").config
local fts = config.ft_func()
-- add all last.
table.insert(fts, "all")
return deduplicate(redirect_filetypes(fts))
end
local function pos_add(p1, p2)
return { p1[1] + p2[1], p1[2] + p2[2] }
end
local function pos_sub(p1, p2)
return { p1[1] - p2[1], p1[2] - p2[2] }
end
local function pop_front(list)
local front = list[1]
for i = 2, #list do
list[i - 1] = list[i]
end
list[#list] = nil
return front
end
local function sorted_keys(t)
local s = {}
local i = 1
for k, _ in pairs(t) do
s[i] = k
i = i + 1
end
table.sort(s)
return s
end
-- from https://www.lua.org/pil/19.3.html
local function key_sorted_pairs(t)
local sorted = sorted_keys(t)
local i = 0
return function()
i = i + 1
if sorted[i] == nil then
return nil
else
return sorted[i], t[sorted[i]], i
end
end
end
local function no_region_check_wrap(fn, ...)
session.jump_active = true
-- will run on next tick, after autocommands (especially CursorMoved) for this are done.
vim.schedule(function()
session.jump_active = false
end)
return fn(...)
end
local function id(a)
return a
end
local function no()
return false
end
local function yes()
return true
end
local function reverse_lookup(t)
local rev = {}
for k, v in pairs(t) do
rev[v] = k
end
return rev
end
local function nop() end
local function indx_of(t, v)
for i, value in ipairs(t) do
if v == value then
return i
end
end
return nil
end
local function ternary(cond, if_val, else_val)
if cond == true then
return if_val
else
return else_val
end
end
-- just compare two integers.
local function cmp(i1, i2)
-- lets hope this ends up as one cmp.
if i1 < i2 then
return -1
end
if i1 > i2 then
return 1
end
return 0
end
-- compare two positions, <0 => pos1<pos2, 0 => pos1=pos2, >0 => pos1 > pos2.
local function pos_cmp(pos1, pos2)
-- if row is different it determines result, otherwise the column does.
return 2 * cmp(pos1[1], pos2[1]) + cmp(pos1[2], pos2[2])
end
return {
get_cursor_0ind = get_cursor_0ind,
set_cursor_0ind = set_cursor_0ind,
move_to_mark = move_to_mark,
normal_move_on_insert = normal_move_on_insert,
insert_move_on = insert_move_on,
any_select = any_select,
remove_n_before_cur = remove_n_before_cur,
get_current_line_to_cursor = get_current_line_to_cursor,
line_chars_before = line_chars_before,
mark_pos_equal = mark_pos_equal,
multiline_equal = multiline_equal,
word_under_cursor = word_under_cursor,
put = put,
to_string_table = to_string_table,
wrap_nodes = wrap_nodes,
pos_equal = pos_equal,
dedent = dedent,
indent = indent,
expand_tabs = expand_tabs,
tab_width = tab_width,
buffer_comment_chars = buffer_comment_chars,
string_wrap = string_wrap,
to_line_table = to_line_table,
find_outer_snippet = find_outer_snippet,
redirect_filetypes = redirect_filetypes,
get_snippet_filetypes = get_snippet_filetypes,
json_decode = vim.json.decode,
json_encode = vim.json.encode,
bytecol_to_utfcol = bytecol_to_utfcol,
pos_sub = pos_sub,
pos_add = pos_add,
deduplicate = deduplicate,
pop_front = pop_front,
key_sorted_pairs = key_sorted_pairs,
no_region_check_wrap = no_region_check_wrap,
id = id,
no = no,
yes = yes,
reverse_lookup = reverse_lookup,
nop = nop,
indx_of = indx_of,
ternary = ternary,
pos_cmp = pos_cmp,
}