1

Refresh generated nvim config

This commit is contained in:
2024-06-03 21:11:20 +02:00
parent fd506d4921
commit 50723ef645
2114 changed files with 84528 additions and 44473 deletions

View File

@ -1,63 +0,0 @@
luasnip-absolute-indexer luasnip.txt /*luasnip-absolute-indexer*
luasnip-api luasnip.txt /*luasnip-api*
luasnip-basics luasnip.txt /*luasnip-basics*
luasnip-basics-adding-snippets luasnip.txt /*luasnip-basics-adding-snippets*
luasnip-basics-jump-index luasnip.txt /*luasnip-basics-jump-index*
luasnip-basics-snippet-insertion luasnip.txt /*luasnip-basics-snippet-insertion*
luasnip-choicenode luasnip.txt /*luasnip-choicenode*
luasnip-cleanup luasnip.txt /*luasnip-cleanup*
luasnip-config-options luasnip.txt /*luasnip-config-options*
luasnip-docstring-cache luasnip.txt /*luasnip-docstring-cache*
luasnip-docstrings luasnip.txt /*luasnip-docstrings*
luasnip-dynamicnode luasnip.txt /*luasnip-dynamicnode*
luasnip-events luasnip.txt /*luasnip-events*
luasnip-ext_opts luasnip.txt /*luasnip-ext_opts*
luasnip-extend-decorator luasnip.txt /*luasnip-extend-decorator*
luasnip-extras luasnip.txt /*luasnip-extras*
luasnip-extras-conditions luasnip.txt /*luasnip-extras-conditions*
luasnip-extras-dynamic-lambda luasnip.txt /*luasnip-extras-dynamic-lambda*
luasnip-extras-filetype-functions luasnip.txt /*luasnip-extras-filetype-functions*
luasnip-extras-fmt luasnip.txt /*luasnip-extras-fmt*
luasnip-extras-lambda luasnip.txt /*luasnip-extras-lambda*
luasnip-extras-match luasnip.txt /*luasnip-extras-match*
luasnip-extras-nonempty luasnip.txt /*luasnip-extras-nonempty*
luasnip-extras-on-the-fly-snippets luasnip.txt /*luasnip-extras-on-the-fly-snippets*
luasnip-extras-partial luasnip.txt /*luasnip-extras-partial*
luasnip-extras-postfix-snippet luasnip.txt /*luasnip-extras-postfix-snippet*
luasnip-extras-repeat luasnip.txt /*luasnip-extras-repeat*
luasnip-extras-select_choice luasnip.txt /*luasnip-extras-select_choice*
luasnip-extras-snippet-list luasnip.txt /*luasnip-extras-snippet-list*
luasnip-extras-snippet-location luasnip.txt /*luasnip-extras-snippet-location*
luasnip-extras-treesitter-postfix-snippet luasnip.txt /*luasnip-extras-treesitter-postfix-snippet*
luasnip-functionnode luasnip.txt /*luasnip-functionnode*
luasnip-indentsnippetnode luasnip.txt /*luasnip-indentsnippetnode*
luasnip-insertnode luasnip.txt /*luasnip-insertnode*
luasnip-key-indexer luasnip.txt /*luasnip-key-indexer*
luasnip-loaders luasnip.txt /*luasnip-loaders*
luasnip-loaders-edit_snippets luasnip.txt /*luasnip-loaders-edit_snippets*
luasnip-loaders-lua luasnip.txt /*luasnip-loaders-lua*
luasnip-loaders-snipmate luasnip.txt /*luasnip-loaders-snipmate*
luasnip-loaders-snippet-specific-filetypes luasnip.txt /*luasnip-loaders-snippet-specific-filetypes*
luasnip-loaders-vs-code luasnip.txt /*luasnip-loaders-vs-code*
luasnip-logging luasnip.txt /*luasnip-logging*
luasnip-lsp-snippets luasnip.txt /*luasnip-lsp-snippets*
luasnip-lsp-snippets-snipmate-parser luasnip.txt /*luasnip-lsp-snippets-snipmate-parser*
luasnip-lsp-snippets-transformations luasnip.txt /*luasnip-lsp-snippets-transformations*
luasnip-multisnippet luasnip.txt /*luasnip-multisnippet*
luasnip-node luasnip.txt /*luasnip-node*
luasnip-node-api luasnip.txt /*luasnip-node-api*
luasnip-node-reference luasnip.txt /*luasnip-node-reference*
luasnip-restorenode luasnip.txt /*luasnip-restorenode*
luasnip-snippetnode luasnip.txt /*luasnip-snippetnode*
luasnip-snippetproxy luasnip.txt /*luasnip-snippetproxy*
luasnip-snippets luasnip.txt /*luasnip-snippets*
luasnip-snippets-data luasnip.txt /*luasnip-snippets-data*
luasnip-source luasnip.txt /*luasnip-source*
luasnip-table-of-contents luasnip.txt /*luasnip-table-of-contents*
luasnip-textnode luasnip.txt /*luasnip-textnode*
luasnip-troubleshooting luasnip.txt /*luasnip-troubleshooting*
luasnip-troubleshooting-adding-snippets luasnip.txt /*luasnip-troubleshooting-adding-snippets*
luasnip-variables luasnip.txt /*luasnip-variables*
luasnip-variables-environment-namespaces luasnip.txt /*luasnip-variables-environment-namespaces*
luasnip-variables-lsp-variables luasnip.txt /*luasnip-variables-lsp-variables*
luasnip.txt luasnip.txt /*luasnip.txt*

View File

@ -1,14 +0,0 @@
" Vim filetype plugin for SnipMate snippets (.snippets files)
if exists("b:did_ftplugin")
finish
endif
let b:did_ftplugin = 1
let b:undo_ftplugin = "setl et< sts< cms< fdm< fde<"
" Use hard tabs
setlocal noexpandtab softtabstop=0
setlocal commentstring=#\ %s
setlocal nospell

View File

@ -1 +0,0 @@
/nix/store/qhhsw7qjik5gh1wkai703p6dq0ydp15r-lua5.1-jsregexp-0.0.7-1 /nix/store/mqbhz05llkddfb5wni0m48kw22ixxps4-lua-5.1.5

View File

@ -1,90 +0,0 @@
vim.filetype.add({
extension = { snippets = "snippets" },
})
local function silent_map(mode, lhs, rhs, desc)
vim.keymap.set(mode, lhs, rhs, { silent = true, desc = desc or "" })
end
silent_map("i", "<Plug>luasnip-expand-or-jump", function()
require("luasnip").expand_or_jump()
end, "LuaSnip: Expand or jump in the current snippet")
silent_map("i", "<Plug>luasnip-expand-snippet", function()
require("luasnip").expand()
end, "LuaSnip: Expand the current snippet")
silent_map("i", "<Plug>luasnip-next-choice", function()
require("luasnip").change_choice(1)
end, "LuaSnip: Change to the next choice from the choiceNode")
silent_map("i", "<Plug>luasnip-prev-choice", function()
require("luasnip").change_choice(-1)
end, "LuaSnip: Change to the previous choice from the choiceNode")
silent_map("i", "<Plug>luasnip-jump-next", function()
require("luasnip").jump(1)
end, "LuaSnip: Jump to the next node")
silent_map("i", "<Plug>luasnip-jump-prev", function()
require("luasnip").jump(-1)
end, "LuaSnip: Jump to the previous node")
silent_map("n", "<Plug>luasnip-delete-check", function()
require("luasnip").unlink_current_if_deleted()
end, "LuaSnip: Removes current snippet from jumplist")
silent_map("!", "<Plug>luasnip-delete-check", function()
require("luasnip").unlink_current_if_deleted()
end, "LuaSnip: Removes current snippet from jumplist")
silent_map("", "<Plug>luasnip-expand-repeat", function()
require("luasnip").expand_repeat()
end, "LuaSnip: Repeat last node expansion")
silent_map("!", "<Plug>luasnip-expand-repeat", function()
require("luasnip").expand_repeat()
end, "LuaSnip: Repeat last node expansion")
silent_map("s", "<Plug>luasnip-expand-or-jump", function()
require("luasnip").expand_or_jump()
end, "LuaSnip: Expand or jump in the current snippet")
silent_map("s", "<Plug>luasnip-expand-snippet", function()
require("luasnip").expand()
end, "LuaSnip: Expand the current snippet")
silent_map("s", "<Plug>luasnip-next-choice", function()
require("luasnip").change_choice(1)
end, "LuaSnip: Change to the next choice from the choiceNode")
silent_map("s", "<Plug>luasnip-prev-choice", function()
require("luasnip").change_choice(-1)
end, "LuaSnip: Change to the previous choice from the choiceNode")
silent_map("s", "<Plug>luasnip-jump-next", function()
require("luasnip").jump(1)
end, "LuaSnip: Jump to the next node")
silent_map("s", "<Plug>luasnip-jump-prev", function()
require("luasnip").jump(-1)
end, "LuaSnip: Jump to the previous node")
vim.api.nvim_create_user_command("LuaSnipUnlinkCurrent", function()
require("luasnip").unlink_current()
end, { force = true })
--stylua: ignore
vim.api.nvim_create_user_command("LuaSnipListAvailable", function()
(
(
vim.version
and type(vim.version) == "table"
and (
((vim.version().major == 0) and (vim.version().minor >= 9))
or (vim.version().major > 0) )
) and vim.print
or vim.pretty_print
)(require("luasnip").available())
end, { force = true })
require("luasnip.config")._setup()
-- register these during startup so lazy_load will also load filetypes whose
-- events fired only before lazy_load is actually called.
-- (BufWinEnter -> lazy_load() wouldn't load any files without these).
vim.api.nvim_create_augroup("_luasnip_lazy_load", {})
vim.api.nvim_create_autocmd({ "BufWinEnter", "FileType" }, {
callback = function(event)
require("luasnip.loaders").load_lazy_loaded(tonumber(event.buf))
end,
group = "_luasnip_lazy_load",
})

View File

@ -1,23 +0,0 @@
function! luasnip#expandable()
return luaeval('require("luasnip").expandable()')
endfunction
function! luasnip#expand_or_jumpable()
return luaeval('require("luasnip").expand_or_jumpable()')
endfunction
function! luasnip#expand_or_locally_jumpable()
return luaeval('require("luasnip").expand_or_locally_jumpable()')
endfunction
function! luasnip#locally_jumpable(direction)
return luaeval('require("luasnip").locally_jumpable(_A)', a:direction)
endfunction
function! luasnip#jumpable(direction)
return luaeval('require("luasnip").jumpable(_A)', a:direction)
endfunction
function! luasnip#choice_active()
return luaeval('require("luasnip").choice_active()')
endfunction

View File

@ -1,29 +0,0 @@
" Syntax highlighting for .snippets files
" Hopefully this should make snippets a bit nicer to write!
syn match snipComment '^#.*'
syn match placeHolder '\${\d\+\(:.\{-}\)\=}' contains=snipCommand
syn match tabStop '\$\d\+'
syn match snipEscape '\\\\\|\\`'
syn match snipCommand '\%(\\\@<!\%(\\\\\)*\)\@<=`.\{-}\%(\\\@<!\%(\\\\\)*\)\@<=`'
syn match snippet '^snippet.*' contains=multiSnipText,snipKeyword
syn match snippet '^autosnippet.*' contains=multiSnipText,snipKeyword
syn match snippet '^extends.*' contains=snipKeyword
syn match snippet '^version.*' contains=snipKeyword
syn match snippet '^priority.*' contains=snipKeyword,priority
syn match priority '\d\+' contained
syn match multiSnipText '\S\+ \zs.*' contained
syn match snipKeyword '^(snippet|extends|version|autosnippet|priority)'me=s+8 contained
" normally we'd want a \s in that group, but that doesn't work => cover common
" cases with \t and " ".
syn match snipError "^[^#vsaep\t ].*$"
hi link snippet Identifier
hi link snipComment Comment
hi link multiSnipText String
hi link snipKeyword Keyword
hi link snipEscape SpecialChar
hi link placeHolder Special
hi link tabStop Special
hi link snipCommand String
hi link snipError Error
hi link priority Number

View File

@ -1,8 +0,0 @@
---@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

@ -1,143 +0,0 @@
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

@ -1,213 +0,0 @@
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

@ -1,26 +0,0 @@
---@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

@ -1,377 +0,0 @@
-- 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

@ -1,94 +0,0 @@
-- 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

@ -1,433 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,55 +0,0 @@
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

@ -1,21 +0,0 @@
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

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

View File

@ -1,89 +0,0 @@
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

@ -1,231 +0,0 @@
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

@ -1,164 +0,0 @@
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

@ -1,89 +0,0 @@
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

@ -1,75 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,190 +0,0 @@
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

@ -1,96 +0,0 @@
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

@ -1,324 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,896 +0,0 @@
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

@ -1,27 +0,0 @@
--- 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

@ -1,466 +0,0 @@
-- 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

@ -1,512 +0,0 @@
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

@ -1,645 +0,0 @@
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

@ -1,723 +0,0 @@
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

@ -1,158 +0,0 @@
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

@ -1,93 +0,0 @@
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

@ -1,8 +0,0 @@
--- @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

@ -1,299 +0,0 @@
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

@ -1,27 +0,0 @@
-- 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

@ -1,427 +0,0 @@
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

@ -1,73 +0,0 @@
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

@ -1,439 +0,0 @@
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

@ -1,155 +0,0 @@
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

@ -1,330 +0,0 @@
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

@ -1,12 +0,0 @@
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

@ -1,117 +0,0 @@
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

@ -1,643 +0,0 @@
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

@ -1,307 +0,0 @@
-- 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

@ -1,125 +0,0 @@
-- 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

@ -1,71 +0,0 @@
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

@ -1,789 +0,0 @@
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

@ -1,132 +0,0 @@
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

@ -1,35 +0,0 @@
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

@ -1,52 +0,0 @@
-- 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

@ -1,328 +0,0 @@
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

@ -1,38 +0,0 @@
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

@ -1,208 +0,0 @@
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

@ -1,54 +0,0 @@
-- 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

@ -1,63 +0,0 @@
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

@ -1,263 +0,0 @@
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

@ -1,188 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,159 +0,0 @@
-- 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

@ -1,82 +0,0 @@
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

@ -1,49 +0,0 @@
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

@ -1,469 +0,0 @@
-- 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

@ -1,41 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,123 +0,0 @@
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

@ -1,208 +0,0 @@
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

@ -1,350 +0,0 @@
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

@ -1,461 +0,0 @@
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

@ -1,141 +0,0 @@
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

@ -1,207 +0,0 @@
-- 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

@ -1,479 +0,0 @@
-- 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

@ -1,266 +0,0 @@
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

@ -1,137 +0,0 @@
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

@ -1,141 +0,0 @@
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

@ -1,141 +0,0 @@
-- 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

@ -1,39 +0,0 @@
---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

@ -1,13 +0,0 @@
-- 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

@ -1,34 +0,0 @@
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

@ -1,522 +0,0 @@
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,
}

View File

@ -1,14 +0,0 @@
" Vim filetype plugin for SnipMate snippets (.snippets files)
if exists("b:did_ftplugin")
finish
endif
let b:did_ftplugin = 1
let b:undo_ftplugin = "setl et< sts< cms< fdm< fde<"
" Use hard tabs
setlocal noexpandtab softtabstop=0
setlocal commentstring=#\ %s
setlocal nospell

View File

@ -1,37 +0,0 @@
local git_ref = 'v2.3.0'
local modrev = '2.3.0'
local specrev = '1'
local repo_url = 'https://github.com/L3MON4D3/LuaSnip'
rockspec_format = '3.0'
package = 'luasnip'
version = modrev ..'-'.. specrev
description = {
summary = 'Snippet Engine for Neovim written in Lua.',
detailed = '',
labels = { 'lua', 'neovim', 'snippet-engine', 'snippets' } ,
homepage = 'https://github.com/L3MON4D3/LuaSnip',
license = 'Apache-2.0'
}
dependencies = { 'lua >= 5.1', 'jsregexp >= 0.0.5, <= 0.0.7' }
test_dependencies = { }
source = {
url = repo_url .. '/archive/' .. git_ref .. '.zip',
dir = 'LuaSnip-' .. '2.3.0',
}
if modrev == 'scm' or modrev == 'dev' then
source = {
url = repo_url:gsub('https', 'git')
}
end
build = {
type = 'builtin',
copy_directories = { 'doc', 'ftplugin', 'plugin', 'syntax' } ,
}

View File

@ -1,90 +0,0 @@
vim.filetype.add({
extension = { snippets = "snippets" },
})
local function silent_map(mode, lhs, rhs, desc)
vim.keymap.set(mode, lhs, rhs, { silent = true, desc = desc or "" })
end
silent_map("i", "<Plug>luasnip-expand-or-jump", function()
require("luasnip").expand_or_jump()
end, "LuaSnip: Expand or jump in the current snippet")
silent_map("i", "<Plug>luasnip-expand-snippet", function()
require("luasnip").expand()
end, "LuaSnip: Expand the current snippet")
silent_map("i", "<Plug>luasnip-next-choice", function()
require("luasnip").change_choice(1)
end, "LuaSnip: Change to the next choice from the choiceNode")
silent_map("i", "<Plug>luasnip-prev-choice", function()
require("luasnip").change_choice(-1)
end, "LuaSnip: Change to the previous choice from the choiceNode")
silent_map("i", "<Plug>luasnip-jump-next", function()
require("luasnip").jump(1)
end, "LuaSnip: Jump to the next node")
silent_map("i", "<Plug>luasnip-jump-prev", function()
require("luasnip").jump(-1)
end, "LuaSnip: Jump to the previous node")
silent_map("n", "<Plug>luasnip-delete-check", function()
require("luasnip").unlink_current_if_deleted()
end, "LuaSnip: Removes current snippet from jumplist")
silent_map("!", "<Plug>luasnip-delete-check", function()
require("luasnip").unlink_current_if_deleted()
end, "LuaSnip: Removes current snippet from jumplist")
silent_map("", "<Plug>luasnip-expand-repeat", function()
require("luasnip").expand_repeat()
end, "LuaSnip: Repeat last node expansion")
silent_map("!", "<Plug>luasnip-expand-repeat", function()
require("luasnip").expand_repeat()
end, "LuaSnip: Repeat last node expansion")
silent_map("s", "<Plug>luasnip-expand-or-jump", function()
require("luasnip").expand_or_jump()
end, "LuaSnip: Expand or jump in the current snippet")
silent_map("s", "<Plug>luasnip-expand-snippet", function()
require("luasnip").expand()
end, "LuaSnip: Expand the current snippet")
silent_map("s", "<Plug>luasnip-next-choice", function()
require("luasnip").change_choice(1)
end, "LuaSnip: Change to the next choice from the choiceNode")
silent_map("s", "<Plug>luasnip-prev-choice", function()
require("luasnip").change_choice(-1)
end, "LuaSnip: Change to the previous choice from the choiceNode")
silent_map("s", "<Plug>luasnip-jump-next", function()
require("luasnip").jump(1)
end, "LuaSnip: Jump to the next node")
silent_map("s", "<Plug>luasnip-jump-prev", function()
require("luasnip").jump(-1)
end, "LuaSnip: Jump to the previous node")
vim.api.nvim_create_user_command("LuaSnipUnlinkCurrent", function()
require("luasnip").unlink_current()
end, { force = true })
--stylua: ignore
vim.api.nvim_create_user_command("LuaSnipListAvailable", function()
(
(
vim.version
and type(vim.version) == "table"
and (
((vim.version().major == 0) and (vim.version().minor >= 9))
or (vim.version().major > 0) )
) and vim.print
or vim.pretty_print
)(require("luasnip").available())
end, { force = true })
require("luasnip.config")._setup()
-- register these during startup so lazy_load will also load filetypes whose
-- events fired only before lazy_load is actually called.
-- (BufWinEnter -> lazy_load() wouldn't load any files without these).
vim.api.nvim_create_augroup("_luasnip_lazy_load", {})
vim.api.nvim_create_autocmd({ "BufWinEnter", "FileType" }, {
callback = function(event)
require("luasnip.loaders").load_lazy_loaded(tonumber(event.buf))
end,
group = "_luasnip_lazy_load",
})

View File

@ -1,23 +0,0 @@
function! luasnip#expandable()
return luaeval('require("luasnip").expandable()')
endfunction
function! luasnip#expand_or_jumpable()
return luaeval('require("luasnip").expand_or_jumpable()')
endfunction
function! luasnip#expand_or_locally_jumpable()
return luaeval('require("luasnip").expand_or_locally_jumpable()')
endfunction
function! luasnip#locally_jumpable(direction)
return luaeval('require("luasnip").locally_jumpable(_A)', a:direction)
endfunction
function! luasnip#jumpable(direction)
return luaeval('require("luasnip").jumpable(_A)', a:direction)
endfunction
function! luasnip#choice_active()
return luaeval('require("luasnip").choice_active()')
endfunction

View File

@ -1,116 +0,0 @@
rock_manifest = {
doc = {
["luasnip.txt"] = "5014cf9a2bb7c7360fd34a1a02356207"
},
ftplugin = {
["snippets.vim"] = "ecd77fcf256c792985acf12ea7603322"
},
lua = {
luasnip = {
["_types.lua"] = "a1b1fc45d496f8ece3e17dc3541e5f93",
["config.lua"] = "1bb0edf593b14b243b116d70cbb605c9",
["default_config.lua"] = "51eea9c217eed18af81d580129c70461",
extras = {
["_extra_types.lua"] = "b8f4a120d5abe22f0112efdcae358817",
["_lambda.lua"] = "e94a2ad0606ed3c4276a573d4e7ab205",
["_parser_combinator.lua"] = "bacc166557d1b5f9f03aff25a56bc801",
["_treesitter.lua"] = "d9fb19599b9d95edab033fdda0684c32",
conditions = {
["expand.lua"] = "35c3ab55ec8e9916ed7cde31cc807b08",
["init.lua"] = "12f7e4b6fd6b5796c36ce61db5844efd",
["show.lua"] = "0cd4059f6ba5582f409ced580e9fef13"
},
["expand_conditions.lua"] = "6ea7479cea2e5fac95a2045a6a283d4b",
["filetype_functions.lua"] = "bdab365ff7bd2d7d148fdc6b3b78d9b4",
["fmt.lua"] = "014768af82d3e7e58437e41335553eb6",
["init.lua"] = "560335e3043e97a826fc8aee4b250fbc",
["otf.lua"] = "8a95cdb7b582497542069bdd0886776b",
["postfix.lua"] = "5e94359e6642b52d8ef6c9df3a90e167",
["select_choice.lua"] = "8c924f05ee0d55ab9b0d9e5c603e1a52",
["snip_location.lua"] = "bd0f8a7f1c61f6a001fa5781c15839d5",
["snippet_list.lua"] = "fe61183934e0bb966b83461febdd1dcb",
["treesitter_postfix.lua"] = "42a5143ad3c647d292b2183566fd6776"
},
["health.lua"] = "b6bd288f728f6897674347ad46917a5b",
["init.lua"] = "96451aae98dbaf3ece53873298479172",
loaders = {
["data.lua"] = "498490d7dfcf2f0374b0d20f429ba6fb",
["from_lua.lua"] = "78d20ec3694e16581e21ed4948c26385",
["from_snipmate.lua"] = "93e1cdc6e024549d9aa6bc917325de24",
["from_vscode.lua"] = "0ff819764a09a735f9ea8ef51413ae83",
["fs_watchers.lua"] = "b36b9f60988b568602350c41b032f9e6",
["init.lua"] = "d470bc3c7bd4690199cf1c0d214782cf",
["snippet_cache.lua"] = "e2b5cf9a46713fb3f108067100e77e0c",
["types.lua"] = "89e18f0f21c1e77be74c1cbe85757d11",
["util.lua"] = "77a85743643bf4d15cbe91af429908d5"
},
nodes = {
["absolute_indexer.lua"] = "efa73978bd91f2d90f2fc9ef53a9c38c",
["choiceNode.lua"] = "c63618056997ec5aec6524fffff7f2fb",
["duplicate.lua"] = "454e20ad45dbf371aa7d09aa21861f1c",
["dynamicNode.lua"] = "28f4e7a46281dc3a2af0875ffc5ff58c",
["functionNode.lua"] = "cf7cb4efb677a139618fd9255734873e",
["insertNode.lua"] = "a25a723746e7ab5973901855de1d1f11",
["key_indexer.lua"] = "d1c4887dfc10501f09b7851aea25f842",
["multiSnippet.lua"] = "2eab1e75c5ee87096f03db006da31844",
["node.lua"] = "c1d2f45dd25dcf5c1574ff63e0f9e88c",
["restoreNode.lua"] = "9613ce23458968aa12737365dd302be7",
["snippet.lua"] = "d6a31a62f45a460bc642822b6d0244f7",
["snippetProxy.lua"] = "68262858f0f9a20a41640d5a11c43481",
["textNode.lua"] = "c22395ab8305a581f021982cd88e2931",
util = {
["trig_engines.lua"] = "a023c5ca92103478cbf40b7ffe2de903"
},
["util.lua"] = "a6be1172f1b37f2018460900b0ab987d"
},
session = {
["enqueueable_operations.lua"] = "2e4f57314f0573601e35943f56e8d4d8",
["init.lua"] = "213d2ea8110e267278d62f5853151ceb",
snippet_collection = {
["init.lua"] = "2d5015eb7cb5717f5aa44fdeebffbe59",
["source.lua"] = "17f2f0c590d4deb57ae0e7af20c153ec"
}
},
["snippets.lua"] = "d41d8cd98f00b204e9800998ecf8427e",
util = {
["_builtin_vars.lua"] = "cb7e73099c5711556f8df8821ca4a182",
["auto_table.lua"] = "f9c5f84a99e71df229c4b6506a447727",
["dict.lua"] = "83d98b784cfe6ab28c1d3727e7220110",
["directed_graph.lua"] = "7eb06677cf726e6be7d64d470660677c",
["environ.lua"] = "61b0b01947a335f749e854f039ec77ac",
["events.lua"] = "cdac0c08202f1295a0bd9f5ee5909b3b",
["ext_opts.lua"] = "55f3ee33870b070d50c3eae516b4724a",
["extend_decorator.lua"] = "07576b8535b2729c9d70f5ba5b036a92",
["functions.lua"] = "86ccff508ce6b6eeefc455308e7d4994",
["jsonc.lua"] = "94fbde2a919a24f3957d004aaf7d136d",
["jsregexp.lua"] = "59eb40a43fa328e82b086863dcbfa626",
["lazy_table.lua"] = "7b0f31805982e74c3e693fd60ad42ec2",
["log.lua"] = "ffe073da229ae489cc72e576c0ab6bee",
["mark.lua"] = "135f7a32a6f1031ea0eb80688997f3d3",
parser = {
["ast_parser.lua"] = "230087c74af6009d8a858259808f3e51",
["ast_utils.lua"] = "7013bc099f5ed408c4cd49b29e4ce63c",
["init.lua"] = "5ae80471a9893a45b12b77a35ecc8d81",
["neovim_ast.lua"] = "08e136ffd26023ef3172ec2aed4ad2e9",
["neovim_parser.lua"] = "c25f144947bceed6036e3d40b70bdef0"
},
["path.lua"] = "3767ba134238fa42469cfcbcfdf16147",
["pattern_tokenizer.lua"] = "f4f99d27e6a6fb5385f583abc70beaab",
["select.lua"] = "b0a8180922f7995a86ea9df7eabb162e",
["str.lua"] = "06645f5bc876c73af9c4fd3296d620e0",
["table.lua"] = "f4a54a5775133c776d65643be728cfdb",
["time.lua"] = "54483e160266c85209e4399fbfc43e1b",
["types.lua"] = "6605cc2d2293f7080d104c63663c0dac",
["util.lua"] = "ac9ec42f0d014d908ff24c5af8172335"
}
}
},
["luasnip-2.3.0-1.rockspec"] = "51f9eecc66d3003eb668326f1e4a0e68",
plugin = {
["luasnip.lua"] = "189a598faa80a372be83a52dc57fb491",
["luasnip.vim"] = "e3d30107f8659679f6766d579ce5bf56"
},
syntax = {
["snippets.vim"] = "5ea760f9406519fc824e2c941ef4d858"
}
}

View File

@ -1,29 +0,0 @@
" Syntax highlighting for .snippets files
" Hopefully this should make snippets a bit nicer to write!
syn match snipComment '^#.*'
syn match placeHolder '\${\d\+\(:.\{-}\)\=}' contains=snipCommand
syn match tabStop '\$\d\+'
syn match snipEscape '\\\\\|\\`'
syn match snipCommand '\%(\\\@<!\%(\\\\\)*\)\@<=`.\{-}\%(\\\@<!\%(\\\\\)*\)\@<=`'
syn match snippet '^snippet.*' contains=multiSnipText,snipKeyword
syn match snippet '^autosnippet.*' contains=multiSnipText,snipKeyword
syn match snippet '^extends.*' contains=snipKeyword
syn match snippet '^version.*' contains=snipKeyword
syn match snippet '^priority.*' contains=snipKeyword,priority
syn match priority '\d\+' contained
syn match multiSnipText '\S\+ \zs.*' contained
syn match snipKeyword '^(snippet|extends|version|autosnippet|priority)'me=s+8 contained
" normally we'd want a \s in that group, but that doesn't work => cover common
" cases with \t and " ".
syn match snipError "^[^#vsaep\t ].*$"
hi link snippet Identifier
hi link snipComment Comment
hi link multiSnipText String
hi link snipKeyword Keyword
hi link snipEscape SpecialChar
hi link placeHolder Special
hi link tabStop Special
hi link snipCommand String
hi link snipError Error
hi link priority Number