Update generated nvim config
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
||||
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*
|
||||
@ -0,0 +1,14 @@
|
||||
" 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
|
||||
@ -0,0 +1 @@
|
||||
/nix/store/qhhsw7qjik5gh1wkai703p6dq0ydp15r-lua5.1-jsregexp-0.0.7-1 /nix/store/mqbhz05llkddfb5wni0m48kw22ixxps4-lua-5.1.5
|
||||
@ -0,0 +1,90 @@
|
||||
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",
|
||||
})
|
||||
@ -0,0 +1,23 @@
|
||||
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
|
||||
@ -0,0 +1,29 @@
|
||||
" 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
|
||||
@ -0,0 +1,8 @@
|
||||
---@alias LuaSnip.Cursor {[1]: number, [2]: number}
|
||||
|
||||
---@class LuaSnip.MatchRegion 0-based region
|
||||
---@field row integer 0-based row
|
||||
---@field col_range { [1]: integer, [2]: integer } 0-based column range, from-in, to-exclusive
|
||||
|
||||
---@alias LuaSnip.Addable table
|
||||
---Anything that can be passed to ls.add_snippets().
|
||||
@ -0,0 +1,143 @@
|
||||
local ext_util = require("luasnip.util.ext_opts")
|
||||
local session = require("luasnip.session")
|
||||
local conf_defaults = require("luasnip.default_config")
|
||||
|
||||
local function set_snip_env(target_conf_defaults, user_config)
|
||||
if not user_config.snip_env then
|
||||
-- target_conf already contains defaults
|
||||
return
|
||||
end
|
||||
|
||||
-- either "set" or "extend", make sure it does not appear in the final snip_env.
|
||||
local snip_env_behaviour = user_config.snip_env.__snip_env_behaviour ~= nil
|
||||
and user_config.snip_env.__snip_env_behaviour
|
||||
or "extend"
|
||||
assert(
|
||||
snip_env_behaviour == "set" or snip_env_behaviour == "extend",
|
||||
"Unknown __snip_env_behaviour, `" .. snip_env_behaviour .. "`"
|
||||
)
|
||||
user_config.snip_env.__snip_env_behaviour = nil
|
||||
|
||||
if snip_env_behaviour == "set" then
|
||||
target_conf_defaults.snip_env = user_config.snip_env
|
||||
else
|
||||
-- cannot use vim.tbl_extend, since we'd need to transfer the metatable.
|
||||
for k, v in pairs(user_config.snip_env) do
|
||||
target_conf_defaults.snip_env[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- set to nil, to mark that it's handled.
|
||||
user_config.snip_env = nil
|
||||
end
|
||||
|
||||
-- declare here to use in set_config.
|
||||
local c
|
||||
|
||||
c = {
|
||||
set_config = function(user_config)
|
||||
user_config = user_config or {}
|
||||
local conf = vim.deepcopy(conf_defaults)
|
||||
|
||||
-- remove unused highlights from default-ext_opts.
|
||||
ext_util.clear_invalid(conf.ext_opts)
|
||||
conf.ext_opts = ext_util.child_complete(conf.ext_opts)
|
||||
user_config.ext_opts =
|
||||
ext_util.child_complete(user_config.ext_opts or {})
|
||||
ext_util.child_extend(user_config.ext_opts, conf.ext_opts)
|
||||
|
||||
-- use value from update_events, then updateevents.
|
||||
-- also nil updateevents, don't spill it into the main config.
|
||||
user_config.update_events = user_config.update_events
|
||||
or user_config.updateevents
|
||||
user_config.updateevents = nil
|
||||
|
||||
set_snip_env(conf, user_config)
|
||||
|
||||
-- handle legacy-key history.
|
||||
if user_config.history ~= nil then
|
||||
conf.keep_roots = user_config.history
|
||||
conf.link_roots = user_config.history
|
||||
conf.exit_roots = not user_config.history
|
||||
conf.link_children = user_config.history
|
||||
|
||||
-- unset key to prevent handling twice.
|
||||
conf.history = nil
|
||||
end
|
||||
|
||||
for k, v in pairs(user_config) do
|
||||
conf[k] = v
|
||||
end
|
||||
|
||||
session.config = conf
|
||||
c._setup()
|
||||
end,
|
||||
|
||||
_setup = function()
|
||||
local augroup = vim.api.nvim_create_augroup("luasnip", {})
|
||||
|
||||
-- events: string[], or string. if string[], each string is one
|
||||
-- event-name, if string, either one event-name, or multiple delimited by `,`.
|
||||
local function ls_autocmd(events, callback)
|
||||
if type(events) == "string" then
|
||||
-- split on ',' for backwards compatibility.
|
||||
-- remove spaces from string.
|
||||
events = vim.split(events:gsub(" ", ""), ",")
|
||||
end
|
||||
vim.api.nvim_create_autocmd(events, {
|
||||
callback = callback,
|
||||
group = augroup,
|
||||
})
|
||||
end
|
||||
if session.config.delete_check_events ~= nil then
|
||||
ls_autocmd(
|
||||
session.config.delete_check_events,
|
||||
require("luasnip").unlink_current_if_deleted
|
||||
)
|
||||
end
|
||||
ls_autocmd(
|
||||
session.config.update_events,
|
||||
require("luasnip").active_update_dependents
|
||||
)
|
||||
if session.config.region_check_events ~= nil then
|
||||
ls_autocmd(session.config.region_check_events, function()
|
||||
require("luasnip").exit_out_of_region(
|
||||
require("luasnip").session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
)
|
||||
end)
|
||||
end
|
||||
-- Remove buffers' nodes on deletion+wipeout.
|
||||
ls_autocmd({ "BufDelete", "BufWipeout" }, function(event)
|
||||
local current_nodes = require("luasnip").session.current_nodes
|
||||
if current_nodes then
|
||||
current_nodes[event.buf] = nil
|
||||
end
|
||||
end)
|
||||
if session.config.enable_autosnippets then
|
||||
ls_autocmd("InsertCharPre", function()
|
||||
Luasnip_just_inserted = true
|
||||
end)
|
||||
ls_autocmd({ "TextChangedI", "TextChangedP" }, function()
|
||||
if Luasnip_just_inserted then
|
||||
require("luasnip").expand_auto()
|
||||
Luasnip_just_inserted = nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if session.config.store_selection_keys then
|
||||
vim.cmd(
|
||||
string.format(
|
||||
[[xnoremap <silent> %s %s]],
|
||||
session.config.store_selection_keys,
|
||||
require("luasnip.util.select").select_keys
|
||||
)
|
||||
)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
-- Keep these two for backward compativility
|
||||
c.setup = c.set_config
|
||||
|
||||
return c
|
||||
@ -0,0 +1,213 @@
|
||||
local types = require("luasnip.util.types")
|
||||
local lazy_table = require("luasnip.util.lazy_table")
|
||||
|
||||
local ft_functions = require("luasnip.extras.filetype_functions")
|
||||
|
||||
-- Inserts a insert(1) before all other nodes, decreases node.pos's as indexing is "wrong".
|
||||
local function modify_nodes(snip)
|
||||
for i = #snip.nodes, 1, -1 do
|
||||
snip.nodes[i + 1] = snip.nodes[i]
|
||||
local node = snip.nodes[i + 1]
|
||||
if node.pos then
|
||||
node.pos = node.pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
local iNode = require("luasnip.nodes.insertNode")
|
||||
snip.nodes[1] = iNode.I(1)
|
||||
end
|
||||
|
||||
local lazy_snip_env = {
|
||||
s = function()
|
||||
return require("luasnip.nodes.snippet").S
|
||||
end,
|
||||
sn = function()
|
||||
return require("luasnip.nodes.snippet").SN
|
||||
end,
|
||||
isn = function()
|
||||
return require("luasnip.nodes.snippet").ISN
|
||||
end,
|
||||
t = function()
|
||||
return require("luasnip.nodes.textNode").T
|
||||
end,
|
||||
i = function()
|
||||
return require("luasnip.nodes.insertNode").I
|
||||
end,
|
||||
f = function()
|
||||
return require("luasnip.nodes.functionNode").F
|
||||
end,
|
||||
c = function()
|
||||
return require("luasnip.nodes.choiceNode").C
|
||||
end,
|
||||
d = function()
|
||||
return require("luasnip.nodes.dynamicNode").D
|
||||
end,
|
||||
r = function()
|
||||
return require("luasnip.nodes.restoreNode").R
|
||||
end,
|
||||
events = function()
|
||||
return require("luasnip.util.events")
|
||||
end,
|
||||
k = function()
|
||||
return require("luasnip.nodes.key_indexer").new_key
|
||||
end,
|
||||
ai = function()
|
||||
return require("luasnip.nodes.absolute_indexer")
|
||||
end,
|
||||
extras = function()
|
||||
return require("luasnip.extras")
|
||||
end,
|
||||
l = function()
|
||||
return require("luasnip.extras").lambda
|
||||
end,
|
||||
rep = function()
|
||||
return require("luasnip.extras").rep
|
||||
end,
|
||||
p = function()
|
||||
return require("luasnip.extras").partial
|
||||
end,
|
||||
m = function()
|
||||
return require("luasnip.extras").match
|
||||
end,
|
||||
n = function()
|
||||
return require("luasnip.extras").nonempty
|
||||
end,
|
||||
dl = function()
|
||||
return require("luasnip.extras").dynamic_lambda
|
||||
end,
|
||||
fmt = function()
|
||||
return require("luasnip.extras.fmt").fmt
|
||||
end,
|
||||
fmta = function()
|
||||
return require("luasnip.extras.fmt").fmta
|
||||
end,
|
||||
conds = function()
|
||||
return require("luasnip.extras.expand_conditions")
|
||||
end,
|
||||
postfix = function()
|
||||
return require("luasnip.extras.postfix").postfix
|
||||
end,
|
||||
types = function()
|
||||
return require("luasnip.util.types")
|
||||
end,
|
||||
parse = function()
|
||||
return require("luasnip.util.parser").parse_snippet
|
||||
end,
|
||||
ms = function()
|
||||
return require("luasnip.nodes.multiSnippet").new_multisnippet
|
||||
end,
|
||||
}
|
||||
|
||||
-- stylua: ignore
|
||||
return {
|
||||
-- corresponds to legacy "history=false".
|
||||
keep_roots = false,
|
||||
link_roots = false,
|
||||
exit_roots = true,
|
||||
link_children = false,
|
||||
|
||||
update_events = "InsertLeave",
|
||||
-- see :h User, event should never be triggered(except if it is `doautocmd`'d)
|
||||
region_check_events = nil,
|
||||
delete_check_events = nil,
|
||||
store_selection_keys = nil, -- Supossed to be the same as the expand shortcut
|
||||
ext_opts = {
|
||||
[types.textNode] = {
|
||||
active = { hl_group = "LuasnipTextNodeActive" },
|
||||
passive = { hl_group = "LuasnipTextNodePassive" },
|
||||
visited = { hl_group = "LuasnipTextNodeVisited" },
|
||||
unvisited = { hl_group = "LuasnipTextNodeUnvisited" },
|
||||
snippet_passive = { hl_group = "LuasnipTextNodeSnippetPassive" },
|
||||
},
|
||||
[types.insertNode] = {
|
||||
active = { hl_group = "LuasnipInsertNodeActive" },
|
||||
passive = { hl_group = "LuasnipInsertNodePassive" },
|
||||
visited = { hl_group = "LuasnipInsertNodeVisited" },
|
||||
unvisited = { hl_group = "LuasnipInsertNodeUnvisited" },
|
||||
snippet_passive = {
|
||||
hl_group = "LuasnipInsertNodeSnippetPassive",
|
||||
},
|
||||
},
|
||||
[types.exitNode] = {
|
||||
active = { hl_group = "LuasnipExitNodeActive" },
|
||||
passive = { hl_group = "LuasnipExitNodePassive" },
|
||||
visited = { hl_group = "LuasnipExitNodeVisited" },
|
||||
unvisited = { hl_group = "LuasnipExitNodeUnvisited" },
|
||||
snippet_passive = { hl_group = "LuasnipExitNodeSnippetPassive" },
|
||||
},
|
||||
[types.functionNode] = {
|
||||
active = { hl_group = "LuasnipFunctionNodeActive" },
|
||||
passive = { hl_group = "LuasnipFunctionNodePassive" },
|
||||
visited = { hl_group = "LuasnipFunctionNodeVisited" },
|
||||
unvisited = { hl_group = "LuasnipFunctionNodeUnvisited" },
|
||||
snippet_passive = {
|
||||
hl_group = "LuasnipFunctionNodeSnippetPassive",
|
||||
},
|
||||
},
|
||||
[types.snippetNode] = {
|
||||
active = { hl_group = "LuasnipSnippetNodeActive" },
|
||||
passive = { hl_group = "LuasnipSnippetNodePassive" },
|
||||
visited = { hl_group = "LuasnipSnippetNodeVisited" },
|
||||
unvisited = { hl_group = "LuasnipSnippetNodeUnvisited" },
|
||||
snippet_passive = {
|
||||
hl_group = "LuasnipSnippetNodeSnippetPassive",
|
||||
},
|
||||
},
|
||||
[types.choiceNode] = {
|
||||
active = { hl_group = "LuasnipChoiceNodeActive" },
|
||||
passive = { hl_group = "LuasnipChoiceNodePassive" },
|
||||
visited = { hl_group = "LuasnipChoiceNodeVisited" },
|
||||
unvisited = { hl_group = "LuasnipChoiceNodeUnvisited" },
|
||||
snippet_passive = {
|
||||
hl_group = "LuasnipChoiceNodeSnippetPassive",
|
||||
},
|
||||
},
|
||||
[types.dynamicNode] = {
|
||||
active = { hl_group = "LuasnipDynamicNodeActive" },
|
||||
passive = { hl_group = "LuasnipDynamicNodePassive" },
|
||||
visited = { hl_group = "LuasnipDynamicNodeVisited" },
|
||||
unvisited = { hl_group = "LuasnipDynamicNodeUnvisited" },
|
||||
snippet_passive = {
|
||||
hl_group = "LuasnipDynamicNodeSnippetPassive",
|
||||
},
|
||||
},
|
||||
[types.snippet] = {
|
||||
active = { hl_group = "LuasnipSnippetActive" },
|
||||
passive = { hl_group = "LuasnipSnippetPassive" },
|
||||
-- not used!
|
||||
visited = { hl_group = "LuasnipSnippetVisited" },
|
||||
unvisited = { hl_group = "LuasnipSnippetUnvisited" },
|
||||
snippet_passive = { hl_group = "LuasnipSnippetSnippetPassive" },
|
||||
},
|
||||
[types.restoreNode] = {
|
||||
active = { hl_group = "LuasnipRestoreNodeActive" },
|
||||
passive = { hl_group = "LuasnipRestoreNodePassive" },
|
||||
visited = { hl_group = "LuasnipRestoreNodeVisited" },
|
||||
unvisited = { hl_group = "LuasnipRestoreNodeUnvisited" },
|
||||
snippet_passive = {
|
||||
hl_group = "LuasnipRestoreNodeSnippetPassive",
|
||||
},
|
||||
},
|
||||
},
|
||||
ext_base_prio = 200,
|
||||
ext_prio_increase = 9,
|
||||
enable_autosnippets = false,
|
||||
parser_nested_assembler = function(pos, snip)
|
||||
-- only require here, to prevent some upfront load-cost.
|
||||
local iNode = require("luasnip.nodes.insertNode")
|
||||
local cNode = require("luasnip.nodes.choiceNode")
|
||||
|
||||
modify_nodes(snip)
|
||||
snip:init_nodes()
|
||||
snip.pos = nil
|
||||
|
||||
return cNode.C(pos, { snip, iNode.I(nil, { "" }) })
|
||||
end,
|
||||
-- Function expected to return a list of filetypes (or empty list)
|
||||
ft_func = ft_functions.from_filetype,
|
||||
-- fn(bufnr) -> string[] (filetypes).
|
||||
load_ft_func = ft_functions.from_filetype_load,
|
||||
-- globals injected into luasnippet-files.
|
||||
snip_env = lazy_table({}, lazy_snip_env),
|
||||
loaders_store_source = false,
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
---@class LuaSnip.extra.MatchTSNodeOpts Passed in by the user, describes how to
|
||||
---select a node from the tree via a query and captures.
|
||||
---@field query? string A query, as text
|
||||
---@field query_name? string The name of a query (passed to `vim.treesitter.query.get`).
|
||||
---@field query_lang string The language of the query.
|
||||
---@field select? LuaSnip.extra.BuiltinMatchSelector|LuaSnip.extra.MatchSelector
|
||||
---@field match_captures? string|string[]
|
||||
|
||||
---@class LuaSnip.extra.MatchedTSNodeInfo
|
||||
---@field capture_name string
|
||||
---@field node TSNode
|
||||
|
||||
---@alias LuaSnip.extra.BuiltinMatchSelector
|
||||
---| '"any"' # The default selector, selects the first match but not return all captures
|
||||
---| '"shortest"' # Selects the shortest match, return all captures too
|
||||
---| '"longest"' # Selects the longest match, return all captures too
|
||||
|
||||
---Call record repeatedly to record all matches/nodes, retrieve once there are no more matches
|
||||
---@class LuaSnip.extra.MatchSelector
|
||||
---@field record fun(ts_match: TSMatch?, node: TSNode): boolean return true if recording can be aborted
|
||||
--- (because the best match has been found)
|
||||
---@field retrieve fun(): TSMatch?,TSNode? return the best match, as determined by this selector.
|
||||
|
||||
---@alias LuaSnip.extra.MatchTSNodeFunc fun(parser: LuaSnip.extra.TSParser, cursor: LuaSnip.Cursor): LuaSnip.extra.NamedTSMatch?,TSNode?
|
||||
|
||||
---@alias LuaSnip.extra.NamedTSMatch table<string,TSNode>
|
||||
@ -0,0 +1,377 @@
|
||||
-- Mostly borrowed from https://github.com/lunarmodules/Penlight/ with just some changes to use
|
||||
-- neovim internal functions and reformat
|
||||
-- Copyright (C) 2009-2016 Steve Donovan, David Manura.
|
||||
|
||||
local concat, append = table.concat, table.insert
|
||||
local map = vim.tbl_map
|
||||
|
||||
local _DEBUG = rawget(_G, "_DEBUG")
|
||||
|
||||
local function assert_arg(n, val, tp, verify, msg, lev)
|
||||
if type(val) ~= tp then
|
||||
error(
|
||||
("argument %d expected a '%s', got a '%s'"):format(n, tp, type(val)),
|
||||
lev or 2
|
||||
)
|
||||
end
|
||||
if verify and not verify(val) then
|
||||
error(("argument %d: '%s' %s"):format(n, val, msg), lev or 2)
|
||||
end
|
||||
end
|
||||
|
||||
local lambda = {}
|
||||
|
||||
-- metatable for Placeholder Expressions (PE)
|
||||
local _PEMT = {}
|
||||
|
||||
local function P(t)
|
||||
setmetatable(t, _PEMT)
|
||||
return t
|
||||
end
|
||||
|
||||
lambda.PE = P
|
||||
|
||||
local function isPE(obj)
|
||||
return getmetatable(obj) == _PEMT
|
||||
end
|
||||
|
||||
lambda.isPE = isPE
|
||||
|
||||
-- construct a placeholder variable (e.g _1 and _2)
|
||||
local function PH(idx)
|
||||
return P({ op = "X", repr = "args[" .. idx .. "]", index = idx })
|
||||
end
|
||||
|
||||
-- construct a constant placeholder variable (e.g _C1 and _C2)
|
||||
local function CPH(idx)
|
||||
return P({ op = "X", repr = "_C" .. idx, index = idx })
|
||||
end
|
||||
|
||||
lambda._1, lambda._2, lambda._3, lambda._4, lambda._5 =
|
||||
PH(1), PH(2), PH(3), PH(4), PH(5)
|
||||
lambda._0 = P({ op = "X", repr = "...", index = 0 })
|
||||
|
||||
function lambda.Var(name)
|
||||
local ls = vim.split(name, "[%s,]+")
|
||||
local res = {}
|
||||
for i = 1, #ls do
|
||||
append(res, P({ op = "X", repr = ls[i], index = 0 }))
|
||||
end
|
||||
return unpack(res)
|
||||
end
|
||||
|
||||
function lambda._(value)
|
||||
return P({ op = "X", repr = value, index = "wrap" })
|
||||
end
|
||||
|
||||
-- unknown keys are some named variable.
|
||||
setmetatable(lambda, {
|
||||
__index = function(_, key)
|
||||
-- \\n to be correctly interpreted in `load()`.
|
||||
return P({
|
||||
op = "X",
|
||||
repr = "args." .. key,
|
||||
index = 0,
|
||||
})
|
||||
end,
|
||||
})
|
||||
|
||||
local repr
|
||||
|
||||
lambda.Nil = lambda.Var("nil")
|
||||
|
||||
function _PEMT.__index(obj, key)
|
||||
return P({ op = "[]", obj, key })
|
||||
end
|
||||
|
||||
function _PEMT.__call(fun, ...)
|
||||
return P({ op = "()", fun, ... })
|
||||
end
|
||||
|
||||
function _PEMT.__tostring(e)
|
||||
return repr(e)
|
||||
end
|
||||
|
||||
function _PEMT.__unm(arg)
|
||||
return P({ op = "unm", arg })
|
||||
end
|
||||
|
||||
function lambda.Not(arg)
|
||||
return P({ op = "not", arg })
|
||||
end
|
||||
|
||||
function lambda.Len(arg)
|
||||
return P({ op = "#", arg })
|
||||
end
|
||||
|
||||
local function binreg(context, t)
|
||||
for name, op in pairs(t) do
|
||||
rawset(context, name, function(x, y)
|
||||
return P({ op = op, x, y })
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local function import_name(name, fun, context)
|
||||
rawset(context, name, function(...)
|
||||
return P({ op = "()", fun, ... })
|
||||
end)
|
||||
end
|
||||
|
||||
local imported_functions = {}
|
||||
|
||||
local function is_global_table(n)
|
||||
return type(_G[n]) == "table"
|
||||
end
|
||||
|
||||
--- wrap a table of functions. This makes them available for use in
|
||||
-- placeholder expressions.
|
||||
-- @string tname a table name
|
||||
-- @tab context context to put results, defaults to environment of caller
|
||||
function lambda.import(tname, context)
|
||||
assert_arg(
|
||||
1,
|
||||
tname,
|
||||
"string",
|
||||
is_global_table,
|
||||
"arg# 1: not a name of a global table"
|
||||
)
|
||||
local t = _G[tname]
|
||||
context = context or _G
|
||||
for name, fun in pairs(t) do
|
||||
import_name(name, fun, context)
|
||||
imported_functions[fun] = name
|
||||
end
|
||||
end
|
||||
|
||||
--- register a function for use in placeholder expressions.
|
||||
-- @lambda fun a function
|
||||
-- @string[opt] name an optional name
|
||||
-- @return a placeholder functiond
|
||||
function lambda.register(fun, name)
|
||||
assert_arg(1, fun, "function")
|
||||
if name then
|
||||
assert_arg(2, name, "string")
|
||||
imported_functions[fun] = name
|
||||
end
|
||||
return function(...)
|
||||
return P({ op = "()", fun, ... })
|
||||
end
|
||||
end
|
||||
|
||||
function lambda.lookup_imported_name(fun)
|
||||
return imported_functions[fun]
|
||||
end
|
||||
|
||||
local function _arg(...)
|
||||
return ...
|
||||
end
|
||||
|
||||
function lambda.Args(...)
|
||||
return P({ op = "()", _arg, ... })
|
||||
end
|
||||
|
||||
-- binary operators with their precedences (see Lua manual)
|
||||
-- precedences might be incremented by one before use depending on
|
||||
-- left- or right-associativity, space them out
|
||||
local binary_operators = {
|
||||
["or"] = 0,
|
||||
["and"] = 2,
|
||||
["=="] = 4,
|
||||
["~="] = 4,
|
||||
["<"] = 4,
|
||||
[">"] = 4,
|
||||
["<="] = 4,
|
||||
[">="] = 4,
|
||||
[".."] = 6,
|
||||
["+"] = 8,
|
||||
["-"] = 8,
|
||||
["*"] = 10,
|
||||
["/"] = 10,
|
||||
["%"] = 10,
|
||||
["^"] = 14,
|
||||
}
|
||||
|
||||
-- unary operators with their precedences
|
||||
local unary_operators = {
|
||||
["not"] = 12,
|
||||
["#"] = 12,
|
||||
["unm"] = 12,
|
||||
}
|
||||
|
||||
-- comparisons (as prefix functions)
|
||||
binreg(lambda, {
|
||||
And = "and",
|
||||
Or = "or",
|
||||
Eq = "==",
|
||||
Lt = "<",
|
||||
Gt = ">",
|
||||
Le = "<=",
|
||||
Ge = ">=",
|
||||
})
|
||||
|
||||
-- standard binary operators (as metamethods)
|
||||
binreg(_PEMT, {
|
||||
__add = "+",
|
||||
__sub = "-",
|
||||
__mul = "*",
|
||||
__div = "/",
|
||||
__mod = "%",
|
||||
__pow = "^",
|
||||
__concat = "..",
|
||||
})
|
||||
|
||||
binreg(_PEMT, { __eq = "==" })
|
||||
|
||||
--- all elements of a table except the first.
|
||||
-- @tab ls a list-like table.
|
||||
function lambda.tail(ls)
|
||||
assert_arg(1, ls, "table")
|
||||
local res = {}
|
||||
for i = 2, #ls do
|
||||
append(res, ls[i])
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- create a string representation of a placeholder expression.
|
||||
-- @param e a placeholder expression
|
||||
-- @param lastpred not used
|
||||
function repr(e, lastpred)
|
||||
local tail = lambda.tail
|
||||
if isPE(e) then
|
||||
local pred = binary_operators[e.op] or unary_operators[e.op]
|
||||
if pred then
|
||||
-- binary or unary operator
|
||||
local s
|
||||
if binary_operators[e.op] then
|
||||
local left_pred = pred
|
||||
local right_pred = pred
|
||||
if e.op == ".." or e.op == "^" then
|
||||
left_pred = left_pred + 1
|
||||
else
|
||||
right_pred = right_pred + 1
|
||||
end
|
||||
local left_arg = repr(e[1], left_pred)
|
||||
local right_arg = repr(e[2], right_pred)
|
||||
s = left_arg .. " " .. e.op .. " " .. right_arg
|
||||
else
|
||||
local op = e.op == "unm" and "-" or e.op
|
||||
s = op .. " " .. repr(e[1], pred)
|
||||
end
|
||||
if lastpred and lastpred > pred then
|
||||
s = "(" .. s .. ")"
|
||||
end
|
||||
return s
|
||||
else -- either postfix, or a placeholder
|
||||
local ls = map(repr, e)
|
||||
if e.op == "[]" then
|
||||
return ls[1] .. "[" .. ls[2] .. "]"
|
||||
elseif e.op == "()" then
|
||||
local fn
|
||||
if ls[1] ~= nil then -- was _args, undeclared!
|
||||
fn = ls[1]
|
||||
else
|
||||
fn = ""
|
||||
end
|
||||
return fn .. "(" .. concat(tail(ls), ",") .. ")"
|
||||
else
|
||||
return e.repr
|
||||
end
|
||||
end
|
||||
elseif type(e) == "string" then
|
||||
return '"' .. e .. '"'
|
||||
elseif type(e) == "function" then
|
||||
local name = lambda.lookup_imported_name(e)
|
||||
if name then
|
||||
return name
|
||||
else
|
||||
return tostring(e)
|
||||
end
|
||||
else
|
||||
return tostring(e) --should not really get here!
|
||||
end
|
||||
end
|
||||
lambda.repr = repr
|
||||
|
||||
-- collect all the non-PE values in this PE into vlist, and replace each occurence
|
||||
-- with a constant PH (_C1, etc). Return the maximum placeholder index found.
|
||||
local collect_values
|
||||
function collect_values(e, vlist)
|
||||
if isPE(e) then
|
||||
if e.op ~= "X" then
|
||||
local m = 0
|
||||
for i = 1, #e do
|
||||
local subx = e[i]
|
||||
local pe = isPE(subx)
|
||||
if pe then
|
||||
if subx.op == "X" and subx.index == "wrap" then
|
||||
subx = subx.repr
|
||||
pe = false
|
||||
else
|
||||
m = math.max(m, collect_values(subx, vlist))
|
||||
end
|
||||
end
|
||||
if not pe then
|
||||
append(vlist, subx)
|
||||
e[i] = CPH(#vlist)
|
||||
end
|
||||
end
|
||||
return m
|
||||
else -- was a placeholder, it has an index...
|
||||
return e.index
|
||||
end
|
||||
else -- plain value has no placeholder dependence
|
||||
return 0
|
||||
end
|
||||
end
|
||||
lambda.collect_values = collect_values
|
||||
|
||||
--- instantiate a PE into an actual function. First we find the largest placeholder used,
|
||||
-- e.g. _2; from this a list of the formal parameters can be build. Then we collect and replace
|
||||
-- any non-PE values from the PE, and build up a constant binding list.
|
||||
-- Finally, the expression can be compiled, and e.__PE_function is set.
|
||||
-- @param e a placeholder expression
|
||||
-- @return a function
|
||||
function lambda.instantiate(e)
|
||||
local consts, values = {}, {}
|
||||
local rep, err, fun
|
||||
local n = lambda.collect_values(e, values)
|
||||
for i = 1, #values do
|
||||
append(consts, "_C" .. i)
|
||||
if _DEBUG then
|
||||
print(i, values[i])
|
||||
end
|
||||
end
|
||||
|
||||
consts = concat(consts, ",")
|
||||
rep = repr(e)
|
||||
local fstr = ("return function(%s) return function(args) return %s end end"):format(
|
||||
consts,
|
||||
rep
|
||||
)
|
||||
if _DEBUG then
|
||||
print(fstr)
|
||||
end
|
||||
fun, err = load(fstr, "fun")
|
||||
if not fun then
|
||||
return nil, err
|
||||
end
|
||||
fun = fun() -- get wrapper
|
||||
fun = fun(unpack(values)) -- call wrapper (values could be empty)
|
||||
e.__PE_function = fun
|
||||
return fun
|
||||
end
|
||||
|
||||
--- instantiate a PE unless it has already been done.
|
||||
-- @param e a placeholder expression
|
||||
-- @return the function
|
||||
function lambda.I(e)
|
||||
if rawget(e, "__PE_function") then
|
||||
return e.__PE_function
|
||||
else
|
||||
return lambda.instantiate(e)
|
||||
end
|
||||
end
|
||||
|
||||
return lambda
|
||||
@ -0,0 +1,94 @@
|
||||
-- Minimal parser combinator,
|
||||
-- only for internal use so not exposed elsewhere nor documented in the oficial doc
|
||||
--
|
||||
local M = {}
|
||||
|
||||
-- Consumes strings matching a pattern, generates the matched string
|
||||
function M.pattern(pat)
|
||||
return function(text, pos)
|
||||
local s, e = text:find(pat, pos)
|
||||
|
||||
if s then
|
||||
local v = text:sub(s, e)
|
||||
return true, v, pos + #v
|
||||
else
|
||||
return false, nil, pos
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Matches whatever `p matches and generates whatever p generates after
|
||||
-- transforming it with `f
|
||||
function M.map(p, f)
|
||||
return function(text, pos)
|
||||
local succ, val, new_pos = p(text, pos)
|
||||
if succ then
|
||||
return true, f(val), new_pos
|
||||
end
|
||||
return false, nil, pos
|
||||
end
|
||||
end
|
||||
|
||||
-- Matches and generates the same as the first of it's children that matches something
|
||||
function M.any(...)
|
||||
local parsers = { ... }
|
||||
return function(text, pos)
|
||||
for _, p in ipairs(parsers) do
|
||||
local succ, val, new_pos = p(text, pos)
|
||||
if succ then
|
||||
return true, val, new_pos
|
||||
end
|
||||
end
|
||||
return false, nil, pos
|
||||
end
|
||||
end
|
||||
|
||||
-- Matches all what its children do in sequence, generates a table of its children generations
|
||||
function M.seq(...)
|
||||
local parsers = { ... }
|
||||
return function(text, pos)
|
||||
local original_pos = pos
|
||||
local values = {}
|
||||
for _, p in ipairs(parsers) do
|
||||
local succ, val, new_pos = p(text, pos)
|
||||
pos = new_pos
|
||||
if not succ then
|
||||
return false, nil, original_pos
|
||||
end
|
||||
table.insert(values, val)
|
||||
end
|
||||
return true, values, pos
|
||||
end
|
||||
end
|
||||
|
||||
-- Matches cero or more times what it child do in sequence, generates a table with those generations
|
||||
function M.star(p)
|
||||
return function(text, pos)
|
||||
local len = #text
|
||||
local values = {}
|
||||
|
||||
while pos <= len do
|
||||
local succ, val, new_pos = p(text, pos)
|
||||
if succ then
|
||||
pos = new_pos
|
||||
table.insert(values, val)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
return #values > 0, values, pos
|
||||
end
|
||||
end
|
||||
|
||||
-- Consumes a literal string, does not generates
|
||||
function M.literal(t)
|
||||
return function(text, pos)
|
||||
if text:sub(pos, pos + #t - 1) == t then
|
||||
return true, nil, pos + #t
|
||||
else
|
||||
return false, text:sub(pos, pos + #t), pos + #t
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,433 @@
|
||||
local util = require("luasnip.util.util")
|
||||
local tbl = require("luasnip.util.table")
|
||||
|
||||
local function get_lang(bufnr)
|
||||
local ft = vim.api.nvim_buf_get_option(bufnr, "ft")
|
||||
local lang = vim.treesitter.language.get_lang(ft) or ft
|
||||
return lang
|
||||
end
|
||||
|
||||
-- Inspect node
|
||||
---@param node TSNode?
|
||||
---@return string
|
||||
local function inspect_node(node)
|
||||
if node == nil then
|
||||
return "nil"
|
||||
end
|
||||
|
||||
local start_row, start_col, end_row, end_col =
|
||||
vim.treesitter.get_node_range(node)
|
||||
|
||||
return ("%s [%d, %d] [%d, %d]"):format(
|
||||
node:type(),
|
||||
start_row,
|
||||
start_col,
|
||||
end_row,
|
||||
end_col
|
||||
)
|
||||
end
|
||||
|
||||
---@param bufnr number
|
||||
---@param region LuaSnip.MatchRegion
|
||||
---@return LanguageTree, string
|
||||
local function reparse_buffer_after_removing_match(bufnr, region)
|
||||
local lang = get_lang(bufnr)
|
||||
|
||||
-- have to get entire buffer, a pattern-match may include lines behind the trigger.
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
-- region is 0-indexed, lines and strings 1-indexed.
|
||||
local region_line = lines[region.row + 1]
|
||||
-- sub includes end, want to exclude it.
|
||||
local left_part = region_line:sub(1, region.col_range[1] + 1 - 1)
|
||||
local right_part = region_line:sub(region.col_range[2] + 1)
|
||||
|
||||
lines[region.row + 1] = left_part .. right_part
|
||||
|
||||
local source = table.concat(lines, "\n")
|
||||
|
||||
---@type LanguageTree
|
||||
local parser = vim.treesitter.get_string_parser(source, lang, nil)
|
||||
parser:parse()
|
||||
return parser, source
|
||||
end
|
||||
|
||||
---@class LuaSnip.extra.FixBufferContext
|
||||
---@field ori_bufnr number
|
||||
---@field ori_text string
|
||||
---@field region LuaSnip.MatchRegion
|
||||
local FixBufferContext = {}
|
||||
|
||||
---@param ori_bufnr number
|
||||
---@param region LuaSnip.MatchRegion
|
||||
---@return LuaSnip.extra.FixBufferContext
|
||||
function FixBufferContext.new(ori_bufnr, region, region_content)
|
||||
local o = {
|
||||
ori_bufnr = ori_bufnr,
|
||||
ori_text = region_content,
|
||||
region = region,
|
||||
}
|
||||
setmetatable(o, {
|
||||
__index = FixBufferContext,
|
||||
})
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function FixBufferContext:enter()
|
||||
vim.api.nvim_buf_set_text(
|
||||
self.ori_bufnr,
|
||||
self.region.row,
|
||||
self.region.col_range[1],
|
||||
self.region.row,
|
||||
self.region.col_range[2],
|
||||
{ "" }
|
||||
)
|
||||
|
||||
local parser, source =
|
||||
vim.treesitter.get_parser(self.ori_bufnr), self.ori_bufnr
|
||||
parser:parse()
|
||||
|
||||
return parser, source
|
||||
end
|
||||
|
||||
function FixBufferContext:leave()
|
||||
vim.api.nvim_buf_set_text(
|
||||
self.ori_bufnr,
|
||||
self.region.row,
|
||||
self.region.col_range[1],
|
||||
self.region.row,
|
||||
self.region.col_range[1],
|
||||
{ self.ori_text }
|
||||
)
|
||||
|
||||
-- The cursor does not necessarily move with the insertion, and has to be
|
||||
-- restored manually.
|
||||
-- when making this work for expansion away from cursor, store cursor-pos
|
||||
-- in self.
|
||||
vim.api.nvim_win_set_cursor(
|
||||
0,
|
||||
{ self.region.row + 1, self.region.col_range[2] }
|
||||
)
|
||||
|
||||
local parser, source =
|
||||
vim.treesitter.get_parser(self.ori_bufnr), self.ori_bufnr
|
||||
parser:parse()
|
||||
return parser, source
|
||||
end
|
||||
|
||||
-- iterate over all
|
||||
local function captures_iter(captures)
|
||||
-- turn string/string[] into map: string -> bool, for querying whether some
|
||||
-- string is present in captures.
|
||||
local capture_map = tbl.list_to_set(captures)
|
||||
|
||||
-- receives the query and the iterator over all its matches.
|
||||
return function(query, match_iter)
|
||||
local current_match
|
||||
local current_capture_id
|
||||
local iter
|
||||
local pattern
|
||||
|
||||
iter = function()
|
||||
-- if there is no current match to continue,
|
||||
if not current_match then
|
||||
pattern, current_match, _ = match_iter()
|
||||
|
||||
-- occurs once there are no more matches.
|
||||
if not pattern then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
while true do
|
||||
local node
|
||||
current_capture_id, node =
|
||||
next(current_match, current_capture_id)
|
||||
if not current_capture_id then
|
||||
break
|
||||
end
|
||||
|
||||
local capture_name = query.captures[current_capture_id]
|
||||
|
||||
if capture_map[capture_name] then
|
||||
return current_match, node
|
||||
end
|
||||
end
|
||||
|
||||
-- iterated over all captures of the current match, reset it to
|
||||
-- retrieve the next match in the recursion.
|
||||
current_match = nil
|
||||
|
||||
-- tail-call-optimization! :fingers_crossed:
|
||||
return iter()
|
||||
end
|
||||
|
||||
return iter
|
||||
end
|
||||
end
|
||||
|
||||
local builtin_tsnode_selectors = {
|
||||
any = function()
|
||||
local best_node
|
||||
local best_node_match
|
||||
return {
|
||||
record = function(match, node)
|
||||
best_node = node
|
||||
best_node_match = match
|
||||
-- abort immediately, we just want any match.
|
||||
return true
|
||||
end,
|
||||
retrieve = function()
|
||||
return best_node_match, best_node
|
||||
end,
|
||||
}
|
||||
end,
|
||||
shortest = function()
|
||||
local best_node
|
||||
local best_node_match
|
||||
|
||||
-- end is already equal, only have to compare start.
|
||||
local best_node_start
|
||||
return {
|
||||
record = function(match, node)
|
||||
local start_row, start_col, _, _ =
|
||||
vim.treesitter.get_node_range(node)
|
||||
if
|
||||
(best_node == nil)
|
||||
or (start_row > best_node_start[1])
|
||||
or (
|
||||
start_row == best_node_start[1]
|
||||
and start_col > best_node_start[2]
|
||||
)
|
||||
then
|
||||
best_node = node
|
||||
best_node_match = match
|
||||
best_node_start = { start_row, start_col }
|
||||
end
|
||||
-- don't abort, have to see all potential nodes to find shortest match.
|
||||
return false
|
||||
end,
|
||||
retrieve = function()
|
||||
return best_node_match, best_node
|
||||
end,
|
||||
}
|
||||
end,
|
||||
longest = function()
|
||||
local best_node
|
||||
local best_node_match
|
||||
|
||||
-- end is already equal, only have to compare start.
|
||||
local best_node_start
|
||||
return {
|
||||
record = function(match, node)
|
||||
local start_row, start_col, _, _ =
|
||||
vim.treesitter.get_node_range(node)
|
||||
if
|
||||
(best_node == nil)
|
||||
or (start_row < best_node_start[1])
|
||||
or (
|
||||
start_row == best_node_start[1]
|
||||
and start_col < best_node_start[2]
|
||||
)
|
||||
then
|
||||
best_node = node
|
||||
best_node_match = match
|
||||
best_node_start = { start_row, start_col }
|
||||
end
|
||||
-- don't abort, have to see all potential nodes to find longest match.
|
||||
return false
|
||||
end,
|
||||
retrieve = function()
|
||||
return best_node_match, best_node
|
||||
end,
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
---@class LuaSnip.extra.TSParser
|
||||
---@field parser LanguageTree
|
||||
---@field source string|number
|
||||
local TSParser = {}
|
||||
|
||||
---@param bufnr number?
|
||||
---@param parser LanguageTree
|
||||
---@param source string|number
|
||||
---@return LuaSnip.extra.TSParser?
|
||||
function TSParser.new(bufnr, parser, source)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
|
||||
local o = {
|
||||
parser = parser,
|
||||
source = source,
|
||||
}
|
||||
|
||||
setmetatable(o, {
|
||||
__index = TSParser,
|
||||
---@param self LuaSnip.extra.TSParser
|
||||
---@return string
|
||||
__tostring = function(self)
|
||||
return ("trees: %d, source: %s"):format(
|
||||
#self.parser:trees(),
|
||||
type(self.source) == "number" and tostring(self.source)
|
||||
or "[COPIED]"
|
||||
)
|
||||
end,
|
||||
})
|
||||
return o
|
||||
end
|
||||
|
||||
---@param pos { [1]: number, [2]: number }?
|
||||
---@return TSNode?
|
||||
function TSParser:get_node_at_pos(pos)
|
||||
pos = vim.F.if_nil(pos, util.get_cursor_0ind())
|
||||
local row, col = pos[1], pos[2]
|
||||
assert(
|
||||
row >= 0 and col >= 0,
|
||||
"Invalid position: row and col must be non-negative"
|
||||
)
|
||||
local range = { row, col, row, col }
|
||||
return self.parser:named_node_for_range(
|
||||
range,
|
||||
{ ignore_injections = false }
|
||||
)
|
||||
end
|
||||
|
||||
---Get the root for the smallest tree containing `pos`.
|
||||
---@param pos { [1]: number, [2]: number }
|
||||
---@return TSNode?
|
||||
function TSParser:root_at(pos)
|
||||
local tree = self.parser:tree_for_range(
|
||||
{ pos[1], pos[2], pos[1], pos[2] },
|
||||
{ ignore_injections = false }
|
||||
)
|
||||
if not tree then
|
||||
return nil
|
||||
end
|
||||
|
||||
return tree:root()
|
||||
end
|
||||
|
||||
---@param match_opts LuaSnip.extra.EffectiveMatchTSNodeOpts
|
||||
---@param pos { [1]: number, [2]: number }
|
||||
---@return LuaSnip.extra.NamedTSMatch?, TSNode?
|
||||
function TSParser:match_at(match_opts, pos)
|
||||
-- Since we want to find a match to the left of pos, and if we accept there
|
||||
-- has to be at least one character (I assume), we should probably not look
|
||||
-- for the tree containing `pos`, since that might be the wrong one (if
|
||||
-- injected languages are in play).
|
||||
local root = self:root_at({ pos[1], pos[2] - 1 })
|
||||
if root == nil then
|
||||
return nil, nil
|
||||
end
|
||||
local root_from_line, _, root_to_line, _ = root:range()
|
||||
|
||||
local query = match_opts.query
|
||||
local selector = match_opts.selector()
|
||||
local next_ts_match =
|
||||
-- end-line is excluded by iter_matches, if the column of root_to
|
||||
-- greater than 0, we would erroneously ignore a line that could
|
||||
-- contain our match.
|
||||
query:iter_matches(root, self.source, root_from_line, root_to_line + 1)
|
||||
|
||||
for match, node in match_opts.generator(query, next_ts_match) do
|
||||
-- false: don't include bytes.
|
||||
local _, _, end_row, end_col = node:range(false)
|
||||
if end_row == pos[1] and end_col == pos[2] then
|
||||
if selector.record(match, node) then
|
||||
-- should abort iteration
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local best_match, node = selector.retrieve()
|
||||
if not best_match then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- map captures via capture-name, not id.
|
||||
local named_captures_match = {}
|
||||
for id, capture_node in pairs(best_match) do
|
||||
named_captures_match[query.captures[id]] = capture_node
|
||||
end
|
||||
return named_captures_match, node
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
---@return string
|
||||
function TSParser:get_node_text(node)
|
||||
-- not sure what happens if this is multiline.
|
||||
return vim.treesitter.get_node_text(node, self.source)
|
||||
end
|
||||
|
||||
---@param root TSNode
|
||||
---@param n number
|
||||
---@param matcher fun(node:TSNode):boolean|nil
|
||||
---@return TSNode?
|
||||
local function find_nth_parent(root, n, matcher)
|
||||
local parent = root
|
||||
matcher = matcher or function()
|
||||
return true
|
||||
end
|
||||
local i = 0
|
||||
while i < n do
|
||||
if not parent or not matcher(parent) then
|
||||
return nil
|
||||
end
|
||||
parent = parent:parent()
|
||||
i = i + 1
|
||||
end
|
||||
if not parent or not matcher(parent) then
|
||||
return nil
|
||||
end
|
||||
return parent
|
||||
end
|
||||
|
||||
---@param root TSNode
|
||||
---@param matcher fun(node:TSNode):boolean|nil
|
||||
local function find_topmost_parent(root, matcher)
|
||||
---@param node TSNode?
|
||||
---@return TSNode?
|
||||
local function _impl(node)
|
||||
if node == nil then
|
||||
return nil
|
||||
end
|
||||
local current = nil
|
||||
if matcher == nil or matcher(node) then
|
||||
current = node
|
||||
end
|
||||
return vim.F.if_nil(_impl(node:parent()), current)
|
||||
end
|
||||
|
||||
return _impl(root)
|
||||
end
|
||||
|
||||
---@param root TSNode
|
||||
---@param matcher fun(node:TSNode):boolean|nil
|
||||
local function find_first_parent(root, matcher)
|
||||
---@param node TSNode?
|
||||
---@return TSNode?
|
||||
local function _impl(node)
|
||||
if node == nil then
|
||||
return nil
|
||||
end
|
||||
if matcher == nil or matcher(node) then
|
||||
return node
|
||||
end
|
||||
return _impl(node:parent())
|
||||
end
|
||||
|
||||
return _impl(root)
|
||||
end
|
||||
|
||||
return {
|
||||
get_lang = get_lang,
|
||||
reparse_buffer_after_removing_match = reparse_buffer_after_removing_match,
|
||||
TSParser = TSParser,
|
||||
FixBufferContext = FixBufferContext,
|
||||
find_topmost_parent = find_topmost_parent,
|
||||
find_first_parent = find_first_parent,
|
||||
find_nth_parent = find_nth_parent,
|
||||
inspect_node = inspect_node,
|
||||
captures_iter = captures_iter,
|
||||
builtin_tsnode_selectors = builtin_tsnode_selectors,
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
local cond_obj = require("luasnip.extras.conditions")
|
||||
|
||||
-- use the functions from show as basis and extend/overwrite functions specific for expand here
|
||||
local M = vim.deepcopy(require("luasnip.extras.conditions.show"))
|
||||
-----------------------
|
||||
-- PRESET CONDITIONS --
|
||||
-----------------------
|
||||
local function line_begin(line_to_cursor, matched_trigger)
|
||||
-- +1 because `string.sub("abcd", 1, -2)` -> abc
|
||||
return line_to_cursor:sub(1, -(#matched_trigger + 1)):match("^%s*$")
|
||||
end
|
||||
M.line_begin = cond_obj.make_condition(line_begin)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,55 @@
|
||||
local M = {}
|
||||
|
||||
-----------------------
|
||||
-- CONDITION OBJECTS --
|
||||
-----------------------
|
||||
local condition_mt = {
|
||||
-- logic operators
|
||||
-- not '-'
|
||||
__unm = function(o1)
|
||||
return M.make_condition(function(...)
|
||||
return not o1(...)
|
||||
end)
|
||||
end,
|
||||
-- or '+'
|
||||
__add = function(o1, o2)
|
||||
return M.make_condition(function(...)
|
||||
return o1(...) or o2(...)
|
||||
end)
|
||||
end,
|
||||
__sub = function(o1, o2)
|
||||
return M.make_condition(function(...)
|
||||
return o1(...) and not o2(...)
|
||||
end)
|
||||
end,
|
||||
-- and '*'
|
||||
__mul = function(o1, o2)
|
||||
return M.make_condition(function(...)
|
||||
return o1(...) and o2(...)
|
||||
end)
|
||||
end,
|
||||
-- xor '^'
|
||||
__pow = function(o1, o2)
|
||||
return M.make_condition(function(...)
|
||||
return o1(...) ~= o2(...)
|
||||
end)
|
||||
end,
|
||||
-- xnor '%'
|
||||
-- might be counter intuitive, but as we can't use '==' (must return bool)
|
||||
-- it's best to use something weird (doesn't have to be used)
|
||||
__mod = function(o1, o2)
|
||||
return function(...)
|
||||
return o1(...) == o2(...)
|
||||
end
|
||||
end,
|
||||
-- use table like a function by overloading __call
|
||||
__call = function(tab, line_to_cursor, matched_trigger, captures)
|
||||
return tab.func(line_to_cursor, matched_trigger, captures)
|
||||
end,
|
||||
}
|
||||
|
||||
function M.make_condition(func)
|
||||
return setmetatable({ func = func }, condition_mt)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,21 @@
|
||||
local cond_obj = require("luasnip.extras.conditions")
|
||||
|
||||
local M = {}
|
||||
-----------------------
|
||||
-- PRESET CONDITIONS --
|
||||
-----------------------
|
||||
local function line_end(line_to_cursor)
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
-- looks pretty inefficient, but as lue interns strings, this is just a
|
||||
-- comparision of pointers (which probably is faster than calculate the
|
||||
-- length and then checking)
|
||||
return line_to_cursor == line
|
||||
end
|
||||
M.line_end = cond_obj.make_condition(line_end)
|
||||
|
||||
local function has_selected_text()
|
||||
return vim.b.LUASNIP_TM_SELECT ~= nil
|
||||
end
|
||||
M.has_selected_text = cond_obj.make_condition(has_selected_text)
|
||||
|
||||
return M
|
||||
@ -0,0 +1 @@
|
||||
return require("luasnip.extras.conditions.expand")
|
||||
@ -0,0 +1,89 @@
|
||||
local function fts_from_ts_lang(lang)
|
||||
local fts = {}
|
||||
-- In case of someone using nvim <= 0.9
|
||||
if vim.treesitter.language and vim.treesitter.language.get_filetypes then
|
||||
fts = vim.treesitter.language.get_filetypes(lang)
|
||||
end
|
||||
-- Keep lang as part of the result, for backward compatibility.
|
||||
-- If lang is already part of fts, one entry will be removed by deduplicate
|
||||
-- in get_snippet_filetypes().
|
||||
table.insert(fts, lang)
|
||||
return fts
|
||||
end
|
||||
|
||||
local function from_cursor_pos()
|
||||
-- get_parser errors if parser not present (no grammar for language).
|
||||
local has_parser, parser = pcall(vim.treesitter.get_parser)
|
||||
|
||||
if has_parser then
|
||||
local cursor = require("luasnip.util.util").get_cursor_0ind()
|
||||
-- assumption: languagetree uses 0-indexed byte-ranges.
|
||||
local lang = parser
|
||||
:language_for_range({
|
||||
cursor[1],
|
||||
cursor[2],
|
||||
cursor[1],
|
||||
cursor[2],
|
||||
})
|
||||
:lang()
|
||||
return fts_from_ts_lang(lang)
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
local function from_filetype()
|
||||
return vim.split(vim.bo.filetype, ".", { plain = true, trimemtpy = false })
|
||||
end
|
||||
|
||||
-- NOTE: Beware that the resulting filetypes may differ from the ones in `vim.bo.filetype`. (for
|
||||
-- example the filetype for LaTeX is 'latex' and not 'tex' as in `vim.bo.filetype`) --
|
||||
local function from_pos_or_filetype()
|
||||
local from_cursor = from_cursor_pos()
|
||||
if not vim.tbl_isempty(from_cursor) then
|
||||
return from_cursor
|
||||
else
|
||||
return from_filetype()
|
||||
end
|
||||
end
|
||||
|
||||
local function from_filetype_load(bufnr)
|
||||
return vim.split(vim.api.nvim_buf_get_option(bufnr, "filetype"), ".", true)
|
||||
end
|
||||
|
||||
local function extend_load_ft(extend_fts)
|
||||
setmetatable(extend_fts, {
|
||||
-- if the filetype is not extended, only it itself should be loaded.
|
||||
-- preventing ifs via __index.
|
||||
__index = function(t, ft)
|
||||
local val = { ft }
|
||||
rawset(t, ft, val)
|
||||
return val
|
||||
end,
|
||||
})
|
||||
|
||||
for ft, _ in pairs(extend_fts) do
|
||||
-- append the regular filetype to the extend-filetypes.
|
||||
table.insert(extend_fts[ft], ft)
|
||||
end
|
||||
|
||||
return function(bufnr)
|
||||
local fts =
|
||||
vim.split(vim.api.nvim_buf_get_option(bufnr, "filetype"), ".", true)
|
||||
local res = {}
|
||||
|
||||
for _, ft in ipairs(fts) do
|
||||
vim.list_extend(res, extend_fts[ft])
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
from_filetype = from_filetype,
|
||||
from_cursor_pos = from_cursor_pos,
|
||||
from_pos_or_filetype = from_pos_or_filetype,
|
||||
from_filetype_load = from_filetype_load,
|
||||
extend_load_ft = extend_load_ft,
|
||||
}
|
||||
@ -0,0 +1,231 @@
|
||||
local text_node = require("luasnip.nodes.textNode").T
|
||||
local wrap_nodes = require("luasnip.util.util").wrap_nodes
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
local Str = require("luasnip.util.str")
|
||||
local rp = require("luasnip.extras").rep
|
||||
|
||||
-- https://gist.github.com/tylerneylon/81333721109155b2d244
|
||||
local function copy3(obj, seen)
|
||||
-- Handle non-tables and previously-seen tables.
|
||||
if type(obj) ~= "table" then
|
||||
return obj
|
||||
end
|
||||
if seen and seen[obj] then
|
||||
return seen[obj]
|
||||
end
|
||||
|
||||
-- New table; mark it as seen an copy recursively.
|
||||
local s = seen or {}
|
||||
local res = {}
|
||||
s[obj] = res
|
||||
for k, v in next, obj do
|
||||
res[copy3(k, s)] = copy3(v, s)
|
||||
end
|
||||
return setmetatable(res, getmetatable(obj))
|
||||
end
|
||||
|
||||
-- Interpolate elements from `args` into format string with placeholders.
|
||||
--
|
||||
-- The placeholder syntax for selecting from `args` is similar to fmtlib and
|
||||
-- Python's .format(), with some notable differences:
|
||||
-- * no format options (like `{:.2f}`)
|
||||
-- * 1-based indexing
|
||||
-- * numbered/auto-numbered placeholders can be mixed; numbered ones set the
|
||||
-- current index to new value, so following auto-numbered placeholders start
|
||||
-- counting from the new value (e.g. `{} {3} {}` is `{1} {3} {4}`)
|
||||
--
|
||||
-- Arguments:
|
||||
-- fmt: string with placeholders
|
||||
-- args: table with list-like and/or map-like keys
|
||||
-- opts:
|
||||
-- delimiters: string, 2 distinct characters (left, right), default "{}"
|
||||
-- strict: boolean, set to false to allow for unused `args`, default true
|
||||
-- repeat_duplicates: boolean, repeat nodes which have jump_index instead of copying them, default false
|
||||
-- Returns: a list of strings and elements of `args` inserted into placeholders
|
||||
local function interpolate(fmt, args, opts)
|
||||
local defaults = {
|
||||
delimiters = "{}",
|
||||
strict = true,
|
||||
repeat_duplicates = false,
|
||||
}
|
||||
opts = vim.tbl_extend("force", defaults, opts or {})
|
||||
|
||||
-- sanitize delimiters
|
||||
assert(
|
||||
#opts.delimiters == 2,
|
||||
'Currently only single-char delimiters are supported, e.g. delimiters="{}" (left, right)'
|
||||
)
|
||||
assert(
|
||||
opts.delimiters:sub(1, 1) ~= opts.delimiters:sub(2, 2),
|
||||
"Delimiters must be two _different_ characters"
|
||||
)
|
||||
local delimiters = {
|
||||
left = opts.delimiters:sub(1, 1),
|
||||
right = opts.delimiters:sub(2, 2),
|
||||
esc_left = vim.pesc(opts.delimiters:sub(1, 1)),
|
||||
esc_right = vim.pesc(opts.delimiters:sub(2, 2)),
|
||||
}
|
||||
|
||||
-- manage insertion of text/args
|
||||
local elements = {}
|
||||
local last_index = 0
|
||||
local used_keys = {}
|
||||
|
||||
local add_text = function(text)
|
||||
if #text > 0 then
|
||||
table.insert(elements, text)
|
||||
end
|
||||
end
|
||||
local add_arg = function(placeholder)
|
||||
local num = tonumber(placeholder)
|
||||
local key
|
||||
if num then -- numbered placeholder
|
||||
last_index = num
|
||||
key = last_index
|
||||
elseif placeholder == "" then -- auto-numbered placeholder
|
||||
key = last_index + 1
|
||||
last_index = key
|
||||
else -- named placeholder
|
||||
key = placeholder
|
||||
end
|
||||
assert(
|
||||
args[key],
|
||||
string.format(
|
||||
"Missing key `%s` in format arguments: `%s`",
|
||||
key,
|
||||
fmt
|
||||
)
|
||||
)
|
||||
-- if the node was already used, insert a copy of it.
|
||||
-- The nodes are modified in-place as part of constructing the snippet,
|
||||
-- modifying one node twice will lead to UB.
|
||||
if used_keys[key] then
|
||||
local jump_index = args[key]:get_jump_index() -- For nodes that don't have a jump index, copy it instead
|
||||
if not opts.repeat_duplicates or jump_index == nil then
|
||||
table.insert(elements, copy3(args[key]))
|
||||
else
|
||||
table.insert(elements, rp(jump_index))
|
||||
end
|
||||
else
|
||||
table.insert(elements, args[key])
|
||||
used_keys[key] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate keeping a range from previous match, e.g. (not in_placeholder vs in_placeholder)
|
||||
-- "Sample {2} string {3}." OR "Sample {2} string {3}."
|
||||
-- left^--------^right OR left^-^right
|
||||
local pattern =
|
||||
string.format("[%s%s]", delimiters.esc_left, delimiters.esc_right)
|
||||
local in_placeholder = false
|
||||
local left = 0
|
||||
|
||||
while true do
|
||||
local right = fmt:find(pattern, left + 1)
|
||||
-- if not found, add the remaining part of string and finish
|
||||
if right == nil then
|
||||
assert(
|
||||
not in_placeholder,
|
||||
string.format('Missing closing delimiter: "%s"', fmt:sub(left))
|
||||
)
|
||||
add_text(fmt:sub(left + 1))
|
||||
break
|
||||
end
|
||||
-- check if the delimiters are escaped
|
||||
local delim = fmt:sub(right, right)
|
||||
local next_char = fmt:sub(right + 1, right + 1)
|
||||
if not in_placeholder and delim == next_char then
|
||||
-- add the previous part of the string with a single delimiter
|
||||
add_text(fmt:sub(left + 1, right))
|
||||
-- and jump over the second one
|
||||
left = right + 1
|
||||
-- "continue"
|
||||
else -- non-escaped delimiter
|
||||
assert(
|
||||
delim
|
||||
== (in_placeholder and delimiters.right or delimiters.left),
|
||||
string.format(
|
||||
'Found unescaped %s %s placeholder; format[%d:%d]="%s"',
|
||||
delim,
|
||||
in_placeholder and "inside" or "outside",
|
||||
left,
|
||||
right,
|
||||
fmt:sub(left, right)
|
||||
)
|
||||
)
|
||||
-- add arg/text depending on current state
|
||||
local add = in_placeholder and add_arg or add_text
|
||||
add(fmt:sub(left + 1, right - 1))
|
||||
-- update state
|
||||
left = right
|
||||
in_placeholder = delim == delimiters.left
|
||||
end
|
||||
end
|
||||
|
||||
-- sanity check: all arguments were used
|
||||
if opts.strict then
|
||||
for key, _ in pairs(args) do
|
||||
assert(
|
||||
used_keys[key],
|
||||
string.format("Unused argument: args[%s]", key)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return elements
|
||||
end
|
||||
|
||||
-- Use a format string with placeholders to interpolate nodes.
|
||||
--
|
||||
-- See `interpolate` documentation for details on the format.
|
||||
--
|
||||
-- Arguments:
|
||||
-- str: format string
|
||||
-- nodes: snippet node or list of nodes
|
||||
-- opts: optional table
|
||||
-- trim_empty: boolean, remove whitespace-only first/last lines, default true
|
||||
-- dedent: boolean, remove all common indent in `str`, default true
|
||||
-- ... the rest is passed to `interpolate`
|
||||
-- Returns: list of snippet nodes
|
||||
local function format_nodes(str, nodes, opts)
|
||||
local defaults = {
|
||||
trim_empty = true,
|
||||
dedent = true,
|
||||
}
|
||||
opts = vim.tbl_extend("force", defaults, opts or {})
|
||||
|
||||
-- allow to pass a single node
|
||||
nodes = wrap_nodes(nodes)
|
||||
|
||||
-- optimization: avoid splitting multiple times
|
||||
local lines = nil
|
||||
|
||||
lines = vim.split(str, "\n", true)
|
||||
Str.process_multiline(lines, opts)
|
||||
str = table.concat(lines, "\n")
|
||||
|
||||
-- pop format_nodes's opts
|
||||
for key, _ in ipairs(defaults) do
|
||||
opts[key] = nil
|
||||
end
|
||||
|
||||
local parts = interpolate(str, nodes, opts)
|
||||
return vim.tbl_map(function(part)
|
||||
-- wrap strings in text nodes
|
||||
if type(part) == "string" then
|
||||
return text_node(vim.split(part, "\n", true))
|
||||
else
|
||||
return part
|
||||
end
|
||||
end, parts)
|
||||
end
|
||||
|
||||
extend_decorator.register(format_nodes, { arg_indx = 3 })
|
||||
|
||||
return {
|
||||
interpolate = interpolate,
|
||||
format_nodes = format_nodes,
|
||||
-- alias
|
||||
fmt = format_nodes,
|
||||
fmta = extend_decorator.apply(format_nodes, { delimiters = "<>" }),
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
local F = require("luasnip.nodes.functionNode").F
|
||||
local SN = require("luasnip.nodes.snippet").SN
|
||||
local D = require("luasnip.nodes.dynamicNode").D
|
||||
local I = require("luasnip.nodes.insertNode").I
|
||||
|
||||
local lambda = {}
|
||||
|
||||
local function _concat(lines)
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
local function make_lambda_args(node_args, imm_parent)
|
||||
local snip = imm_parent.snippet
|
||||
-- turn args' table-multilines into \n-multilines (needs to be possible
|
||||
-- to process args with luas' string-functions).
|
||||
local args = vim.tbl_map(_concat, node_args)
|
||||
|
||||
setmetatable(args, {
|
||||
__index = function(table, key)
|
||||
local val
|
||||
-- key may be capture or env-variable.
|
||||
local num = key:match("CAPTURE(%d+)")
|
||||
if num then
|
||||
val = snip.captures[tonumber(num)]
|
||||
else
|
||||
-- env may be string or table.
|
||||
if type(snip.env[key]) == "table" then
|
||||
-- table- to \n-multiline.
|
||||
val = _concat(snip.env[key])
|
||||
else
|
||||
val = snip.env[key]
|
||||
end
|
||||
end
|
||||
rawset(table, key, val)
|
||||
return val
|
||||
end,
|
||||
})
|
||||
return args
|
||||
end
|
||||
|
||||
local function expr_to_fn(expr)
|
||||
local _lambda = require("luasnip.extras._lambda")
|
||||
|
||||
local fn_code = _lambda.instantiate(expr)
|
||||
local function fn(args, snip)
|
||||
-- to be sure, lambda may end with a `match` returning nil.
|
||||
local out = fn_code(make_lambda_args(args, snip)) or ""
|
||||
return vim.split(out, "\n")
|
||||
end
|
||||
return fn
|
||||
end
|
||||
|
||||
local LM = {}
|
||||
function LM:__index(key)
|
||||
return require("luasnip.extras._lambda")[key]
|
||||
end
|
||||
function LM:__call(expr, input_ids)
|
||||
return F(expr_to_fn(expr), input_ids)
|
||||
end
|
||||
|
||||
setmetatable(lambda, LM)
|
||||
|
||||
local function to_function(val, use_re)
|
||||
if type(val) == "function" then
|
||||
return val
|
||||
end
|
||||
if type(val) == "string" and not use_re then
|
||||
return function()
|
||||
return val
|
||||
end
|
||||
end
|
||||
if type(val) == "string" and use_re then
|
||||
return function(args)
|
||||
return _concat(args[1]):match(val)
|
||||
end
|
||||
end
|
||||
if lambda.isPE(val) then
|
||||
local lmb = lambda.instantiate(val)
|
||||
return function(args, snip)
|
||||
return lmb(make_lambda_args(args, snip))
|
||||
end
|
||||
end
|
||||
assert(false, "Can't convert argument to function")
|
||||
end
|
||||
|
||||
local function match(index, _match, _then, _else)
|
||||
assert(_match, "You have to pass at least 2 arguments")
|
||||
|
||||
_match = to_function(_match, true)
|
||||
_then = to_function(_then or function(args, snip)
|
||||
local match_return = _match(args, snip)
|
||||
return (
|
||||
(
|
||||
type(match_return) == "string"
|
||||
-- _assume_ table of string.
|
||||
or type(match_return) == "table"
|
||||
) and match_return
|
||||
) or ""
|
||||
end)
|
||||
_else = to_function(_else or "")
|
||||
|
||||
local function func(args, snip)
|
||||
local out = nil
|
||||
if _match(args, snip) then
|
||||
out = _then(args, snip)
|
||||
else
|
||||
out = _else(args, snip)
|
||||
end
|
||||
-- \n is used as a line-separator for simple strings.
|
||||
return type(out) == "string" and vim.split(out, "\n") or out
|
||||
end
|
||||
|
||||
return F(func, index)
|
||||
end
|
||||
|
||||
return {
|
||||
lambda = lambda,
|
||||
match = match,
|
||||
-- repeat a node.
|
||||
rep = function(node_indx)
|
||||
return F(function(args)
|
||||
return args[1]
|
||||
end, node_indx)
|
||||
end,
|
||||
-- Insert the output of a function.
|
||||
partial = function(func, ...)
|
||||
return F(function(_, _, ...)
|
||||
return func(...)
|
||||
end, {}, { user_args = { ... } })
|
||||
end,
|
||||
nonempty = function(indx, text_if, text_if_not)
|
||||
assert(
|
||||
type(indx) == "number",
|
||||
"this only checks one node for emptiness!"
|
||||
)
|
||||
assert(
|
||||
text_if,
|
||||
"At least the text for nonemptiness has to be supplied."
|
||||
)
|
||||
|
||||
return F(function(args)
|
||||
return (args[1][1] ~= "" or #args[1] > 1) and text_if
|
||||
or (text_if_not or "")
|
||||
end, {
|
||||
indx,
|
||||
})
|
||||
end,
|
||||
dynamic_lambda = function(pos, lambd, args_indcs)
|
||||
local insert_preset_text_func = lambda.instantiate(lambd)
|
||||
return D(pos, function(args, imm_parent)
|
||||
-- to be sure, lambda may end with a `match` returning nil.
|
||||
local out = insert_preset_text_func(
|
||||
make_lambda_args(args, imm_parent)
|
||||
) or ""
|
||||
return SN(pos, {
|
||||
I(1, vim.split(out, "\n")),
|
||||
})
|
||||
end, args_indcs)
|
||||
end,
|
||||
|
||||
--alias
|
||||
l = lambda,
|
||||
m = match,
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
local ls = require("luasnip")
|
||||
local cp = require("luasnip.util.functions").copy
|
||||
local p = require("luasnip.extras._parser_combinator")
|
||||
local dedent = require("luasnip.util.str").dedent
|
||||
local M = {}
|
||||
|
||||
local T = { EOL = "EOL", TXT = "TXT", INP = "INP" }
|
||||
|
||||
local chunk = p.any(
|
||||
p.map(p.literal("$$"), function()
|
||||
return { T.TXT, "$" }
|
||||
end),
|
||||
p.map(p.literal("\n"), function()
|
||||
return { T.EOL }
|
||||
end),
|
||||
p.map(p.seq(p.literal("$"), p.pattern("%w*")), function(c)
|
||||
return { T.INP, c[1] }
|
||||
end),
|
||||
p.map(p.pattern("[^\n$]*"), function(c)
|
||||
return { T.TXT, c }
|
||||
end)
|
||||
)
|
||||
|
||||
M._snippet_chunks = p.star(chunk)
|
||||
|
||||
function M._txt_to_snip(txt)
|
||||
local t = ls.t
|
||||
local s = ls.s
|
||||
local i = ls.i
|
||||
local f = ls.f
|
||||
txt = dedent(txt)
|
||||
|
||||
-- The parser does not handle empty strings
|
||||
if txt == "" then
|
||||
return s("", t({ "" }))
|
||||
end
|
||||
|
||||
local _, chunks, _ = M._snippet_chunks(txt, 1)
|
||||
|
||||
local current_text_arg = { "" }
|
||||
local nodes = {}
|
||||
local know_inputs = {}
|
||||
local last_input_pos = 0
|
||||
|
||||
for _, part in ipairs(chunks) do
|
||||
if part[1] == T.TXT then
|
||||
current_text_arg[#current_text_arg] = current_text_arg[#current_text_arg]
|
||||
.. part[2]
|
||||
elseif #current_text_arg > 1 or current_text_arg[1] ~= "" then
|
||||
table.insert(nodes, t(current_text_arg))
|
||||
current_text_arg = { "" }
|
||||
end
|
||||
|
||||
if part[1] == T.EOL then
|
||||
table.insert(current_text_arg, "")
|
||||
elseif part[1] == T.INP then
|
||||
local inp_pos = know_inputs[part[2]]
|
||||
if inp_pos then
|
||||
table.insert(nodes, f(cp, { inp_pos }))
|
||||
else
|
||||
last_input_pos = last_input_pos + 1
|
||||
know_inputs[part[2]] = last_input_pos
|
||||
table.insert(nodes, i(last_input_pos, part[2]))
|
||||
end
|
||||
end
|
||||
end
|
||||
if #current_text_arg > 1 or current_text_arg[1] ~= "" then
|
||||
table.insert(nodes, t(current_text_arg))
|
||||
end
|
||||
return s("", nodes)
|
||||
end
|
||||
|
||||
local last_snip = nil
|
||||
local last_reg = nil
|
||||
|
||||
-- Create snippets On The Fly
|
||||
-- It's advaisable not to use the default register as luasnip will probably
|
||||
-- override it
|
||||
function M.on_the_fly(regname)
|
||||
regname = regname or ""
|
||||
local reg = table.concat(vim.fn.getreg(regname, 1, true), "\n") -- Avoid eol in the last line
|
||||
if last_reg ~= reg then
|
||||
last_reg = reg
|
||||
last_snip = M._txt_to_snip(reg)
|
||||
end
|
||||
ls.snip_expand(last_snip)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,75 @@
|
||||
local snip = require("luasnip.nodes.snippet").S
|
||||
local events = require("luasnip.util.events")
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local util = require("luasnip.util.util")
|
||||
|
||||
local matches = {
|
||||
default = [[[%w%.%_%-%"%']+$]],
|
||||
line = "^.+$",
|
||||
}
|
||||
|
||||
local function wrap_resolve_expand_params(match_pattern, user_resolve)
|
||||
return function(snippet, line_to_cursor, match, captures)
|
||||
if line_to_cursor:sub(1, -1 - #match):match(match_pattern) == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local pos = util.get_cursor_0ind()
|
||||
local line_to_cursor_except_match =
|
||||
line_to_cursor:sub(1, #line_to_cursor - #match)
|
||||
local postfix_match = line_to_cursor_except_match:match(match_pattern)
|
||||
or ""
|
||||
local res = {
|
||||
clear_region = {
|
||||
from = { pos[1], pos[2] - #postfix_match - #match },
|
||||
to = pos,
|
||||
},
|
||||
env_override = {
|
||||
POSTFIX_MATCH = postfix_match,
|
||||
},
|
||||
}
|
||||
|
||||
if user_resolve then
|
||||
local user_res =
|
||||
user_resolve(snippet, line_to_cursor, match, captures)
|
||||
if user_res then
|
||||
res = vim.tbl_deep_extend("force", res, user_res, {
|
||||
env_override = {},
|
||||
})
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
end
|
||||
|
||||
local function postfix(context, nodes, opts)
|
||||
opts = opts or {}
|
||||
local user_callback = vim.tbl_get(opts, "callbacks", -1, events.pre_expand)
|
||||
vim.validate({
|
||||
context = { context, { "string", "table" } },
|
||||
nodes = { nodes, "table" },
|
||||
opts = { opts, "table" },
|
||||
user_callback = { user_callback, { "nil", "function" } },
|
||||
})
|
||||
|
||||
context = node_util.wrap_context(context)
|
||||
context.wordTrig = false
|
||||
local match_pattern = context.match_pattern or matches.default
|
||||
context.resolveExpandParams =
|
||||
wrap_resolve_expand_params(match_pattern, context.resolveExpandParams)
|
||||
|
||||
return snip(context, nodes, opts)
|
||||
end
|
||||
extend_decorator.register(
|
||||
postfix,
|
||||
{ arg_indx = 1, extend = node_util.snippet_extend_context },
|
||||
{ arg_indx = 3 }
|
||||
)
|
||||
|
||||
return {
|
||||
postfix = postfix,
|
||||
matches = matches,
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
local session = require("luasnip.session")
|
||||
local ls = require("luasnip")
|
||||
|
||||
local function set_choice_callback(_, indx)
|
||||
if not indx then
|
||||
return
|
||||
end
|
||||
-- feed+immediately execute i to enter INSERT after vim.ui.input closes.
|
||||
vim.api.nvim_feedkeys("i", "x", false)
|
||||
ls.set_choice(indx)
|
||||
end
|
||||
|
||||
local function select_choice()
|
||||
assert(
|
||||
session.active_choice_nodes[vim.api.nvim_get_current_buf()],
|
||||
"No active choiceNode"
|
||||
)
|
||||
vim.ui.select(
|
||||
ls.get_current_choices(),
|
||||
{ kind = "luasnip" },
|
||||
set_choice_callback
|
||||
)
|
||||
end
|
||||
|
||||
return select_choice
|
||||
@ -0,0 +1,190 @@
|
||||
local Source = require("luasnip.session.snippet_collection.source")
|
||||
local util = require("luasnip.util.util")
|
||||
|
||||
-- stylua: ignore
|
||||
local tsquery_parse =
|
||||
(vim.treesitter.query and vim.treesitter.query.parse)
|
||||
and vim.treesitter.query.parse
|
||||
or vim.treesitter.parse_query
|
||||
|
||||
local M = {}
|
||||
|
||||
-- return: 4-tuple, {start_line, start_col, end_line, end_col}, range of
|
||||
-- function-call.
|
||||
local function lua_find_function_call_node_at(bufnr, line)
|
||||
local has_parser, parser = pcall(vim.treesitter.get_parser, bufnr, "lua")
|
||||
if not has_parser then
|
||||
error("Error while getting parser: " .. parser)
|
||||
end
|
||||
|
||||
local root = parser:parse()[1]:root()
|
||||
local query = tsquery_parse("lua", [[(function_call) @f_call]])
|
||||
for _, node, _ in query:iter_captures(root, bufnr, line, line + 300) do
|
||||
if node:range() == line then
|
||||
return { node:range() }
|
||||
end
|
||||
end
|
||||
error(
|
||||
"Query for `(function_call)` starting at line %s did not yield a result."
|
||||
)
|
||||
end
|
||||
|
||||
local function range_highlight(line_start, line_end, hl_duration_ms)
|
||||
-- make sure line_end is also visible.
|
||||
vim.api.nvim_win_set_cursor(0, { line_end, 0 })
|
||||
vim.api.nvim_win_set_cursor(0, { line_start, 0 })
|
||||
|
||||
if hl_duration_ms > 0 then
|
||||
local hl_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- highlight snippet for 1000ms
|
||||
local id = vim.api.nvim_buf_set_extmark(
|
||||
hl_buf,
|
||||
ls.session.ns_id,
|
||||
line_start - 1,
|
||||
0,
|
||||
{
|
||||
-- one line below, at col 0 => entire last line is highlighted.
|
||||
end_row = line_end - 1 + 1,
|
||||
hl_group = "Visual",
|
||||
}
|
||||
)
|
||||
vim.defer_fn(function()
|
||||
vim.api.nvim_buf_del_extmark(hl_buf, ls.session.ns_id, id)
|
||||
end, hl_duration_ms)
|
||||
end
|
||||
end
|
||||
|
||||
local function json_find_snippet_definition(bufnr, filetype, snippet_name)
|
||||
local parser_ok, parser = pcall(vim.treesitter.get_parser, bufnr, filetype)
|
||||
if not parser_ok then
|
||||
error("Error while getting parser: " .. parser)
|
||||
end
|
||||
|
||||
local root = parser:parse()[1]:root()
|
||||
-- don't want to pass through whether this file is json or jsonc, just use
|
||||
-- parser-language.
|
||||
local query = tsquery_parse(
|
||||
parser:lang(),
|
||||
([[
|
||||
(pair
|
||||
key: (string (string_content) @key (#eq? @key "%s"))
|
||||
) @snippet
|
||||
]]):format(snippet_name)
|
||||
)
|
||||
for id, node, _ in query:iter_captures(root, bufnr) do
|
||||
if
|
||||
query.captures[id] == "snippet"
|
||||
and node:parent():parent() == root
|
||||
then
|
||||
-- return first match.
|
||||
return { node:range() }
|
||||
end
|
||||
end
|
||||
|
||||
error(
|
||||
("Treesitter did not find the definition for snippet `%s`"):format(
|
||||
snippet_name
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
local function win_edit(file)
|
||||
vim.api.nvim_command(":e " .. file)
|
||||
end
|
||||
|
||||
function M.jump_to_snippet(snip, opts)
|
||||
opts = opts or {}
|
||||
local hl_duration_ms = opts.hl_duration_ms or 1500
|
||||
local edit_fn = opts.edit_fn or win_edit
|
||||
|
||||
local source = Source.get(snip)
|
||||
if not source then
|
||||
print("Snippet does not have a source.")
|
||||
return
|
||||
end
|
||||
|
||||
edit_fn(source.file)
|
||||
-- assumption: after this, file is the current buffer.
|
||||
|
||||
if source.line and source.line_end then
|
||||
-- happy path: we know both begin and end of snippet-definition.
|
||||
range_highlight(source.line, source.line_end, hl_duration_ms)
|
||||
return
|
||||
end
|
||||
|
||||
local fcall_range
|
||||
local ft = util.ternary(
|
||||
vim.bo[0].filetype ~= "",
|
||||
vim.bo[0].filetype,
|
||||
vim.api.nvim_buf_get_name(0):match("%.([^%.]+)$")
|
||||
)
|
||||
if ft == "lua" then
|
||||
if source.line then
|
||||
-- in lua-file, can get region of definition via treesitter.
|
||||
-- 0: current buffer.
|
||||
local ok
|
||||
ok, fcall_range =
|
||||
pcall(lua_find_function_call_node_at, 0, source.line - 1)
|
||||
if not ok then
|
||||
print(
|
||||
"Could not determine range for snippet-definition: "
|
||||
.. fcall_range
|
||||
)
|
||||
vim.api.nvim_win_set_cursor(0, { source.line, 0 })
|
||||
return
|
||||
end
|
||||
else
|
||||
print("Can't jump to snippet: source does not provide line.")
|
||||
return
|
||||
end
|
||||
-- matches *.json or *.jsonc.
|
||||
elseif ft == "json" or ft == "jsonc" then
|
||||
local ok
|
||||
ok, fcall_range = pcall(json_find_snippet_definition, 0, ft, snip.name)
|
||||
if not ok then
|
||||
print(
|
||||
"Could not determine range of snippet-definition: "
|
||||
.. fcall_range
|
||||
)
|
||||
return
|
||||
end
|
||||
else
|
||||
print(
|
||||
("Don't know how to highlight snippet-definitions in current buffer `%s`.%s"):format(
|
||||
vim.api.nvim_buf_get_name(0),
|
||||
source.line ~= nil and " Jumping to `source.line`" or ""
|
||||
)
|
||||
)
|
||||
|
||||
if source.line ~= nil then
|
||||
vim.api.nvim_win_set_cursor(0, { source.line, 0 })
|
||||
end
|
||||
return
|
||||
end
|
||||
assert(fcall_range ~= nil, "fcall_range is not nil")
|
||||
|
||||
-- 1 is line_from, 3 is line_end.
|
||||
-- +1 since range is row-0-indexed.
|
||||
range_highlight(fcall_range[1] + 1, fcall_range[3] + 1, hl_duration_ms)
|
||||
|
||||
local new_source = Source.from_location(
|
||||
source.file,
|
||||
{ line = fcall_range[1] + 1, line_end = fcall_range[3] + 1 }
|
||||
)
|
||||
Source.set(snip, new_source)
|
||||
end
|
||||
|
||||
function M.jump_to_active_snippet(opts)
|
||||
local active_node =
|
||||
require("luasnip.session").current_nodes[vim.api.nvim_get_current_buf()]
|
||||
if not active_node then
|
||||
print("No active snippet.")
|
||||
return
|
||||
end
|
||||
|
||||
local snip = active_node.parent.snippet
|
||||
M.jump_to_snippet(snip, opts)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,96 @@
|
||||
local available = require("luasnip").available
|
||||
|
||||
local function snip_info(snippet)
|
||||
return {
|
||||
name = snippet.name,
|
||||
trigger = snippet.trigger,
|
||||
description = snippet.description,
|
||||
wordTrig = snippet.wordTrig and true or false,
|
||||
regTrig = snippet.regTrig and true or false,
|
||||
docstring = snippet:get_docstring(),
|
||||
}
|
||||
end
|
||||
|
||||
local function get_name(buf)
|
||||
return "LuaSnip://Snippets"
|
||||
end
|
||||
|
||||
local win_opts = { foldmethod = "indent" }
|
||||
local buf_opts = { filetype = "lua" }
|
||||
|
||||
local function set_win_opts(win, opts)
|
||||
for opt, val in pairs(opts) do
|
||||
vim.api.nvim_win_set_option(win, opt, val)
|
||||
end
|
||||
end
|
||||
|
||||
local function set_buf_opts(buf, opts)
|
||||
for opt, val in pairs(opts) do
|
||||
vim.api.nvim_buf_set_option(buf, opt, val)
|
||||
end
|
||||
end
|
||||
|
||||
local function make_scratch_buf(buf)
|
||||
local opts = {
|
||||
buftype = "nofile",
|
||||
bufhidden = "wipe",
|
||||
buflisted = false,
|
||||
swapfile = false,
|
||||
modified = false,
|
||||
modeline = false,
|
||||
}
|
||||
|
||||
set_buf_opts(buf, opts)
|
||||
end
|
||||
|
||||
local function display_split(opts)
|
||||
opts = opts or {}
|
||||
opts.win_opts = opts.win_opts or win_opts
|
||||
opts.buf_opts = opts.buf_opts or buf_opts
|
||||
opts.get_name = opts.get_name or get_name
|
||||
|
||||
return function(printer_result)
|
||||
-- create and open buffer on right vertical split
|
||||
vim.cmd("botright vnew")
|
||||
|
||||
-- get buf and win handle
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
|
||||
-- make scratch buffer
|
||||
vim.api.nvim_buf_set_name(buf, opts.get_name(buf))
|
||||
make_scratch_buf(buf)
|
||||
|
||||
-- disable diagnostics
|
||||
vim.diagnostic.disable(buf)
|
||||
|
||||
-- set any extra win and buf opts
|
||||
set_win_opts(win, opts.win_opts)
|
||||
set_buf_opts(buf, opts.buf_opts)
|
||||
|
||||
-- dump snippets
|
||||
local replacement = vim.split(printer_result, "\n")
|
||||
vim.api.nvim_buf_set_lines(buf, 0, 0, false, replacement)
|
||||
|
||||
-- make it unmodifiable at this point
|
||||
vim.api.nvim_buf_set_option(buf, "modifiable", false)
|
||||
end
|
||||
end
|
||||
|
||||
local function open(opts)
|
||||
opts = opts or {}
|
||||
opts.snip_info = opts.snip_info or snip_info
|
||||
opts.printer = opts.printer or vim.inspect
|
||||
opts.display = opts.display or display_split()
|
||||
|
||||
-- load snippets before changing windows/buffers
|
||||
local snippets = available(opts.snip_info)
|
||||
|
||||
-- open snippets
|
||||
opts.display(opts.printer(snippets))
|
||||
end
|
||||
|
||||
return {
|
||||
open = open,
|
||||
options = { display = display_split },
|
||||
}
|
||||
@ -0,0 +1,324 @@
|
||||
if vim.version().major == 0 and vim.version().minor < 9 then
|
||||
-- need LanguageTree:tree_for_range and don't want to go through the hassle
|
||||
-- of differentiating multiple version of query.get/parse.
|
||||
error("treesitter_postfix does not support neovim < 0.9")
|
||||
end
|
||||
|
||||
local snip = require("luasnip.nodes.snippet").S
|
||||
local ts = require("luasnip.extras._treesitter")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
local tbl = require("luasnip.util.table")
|
||||
local util = require("luasnip.util.util")
|
||||
|
||||
--- Normalize the arguments passed to treesitter_postfix into a function that
|
||||
--- returns treesitter-matches to the specified query+captures.
|
||||
---@param opts LuaSnip.extra.MatchTSNodeOpts
|
||||
---@return LuaSnip.extra.MatchTSNodeFunc
|
||||
local function generate_match_tsnode_func(opts)
|
||||
local match_opts = {}
|
||||
|
||||
if opts.query then
|
||||
match_opts.query =
|
||||
vim.treesitter.query.parse(opts.query_lang, opts.query)
|
||||
else
|
||||
match_opts.query = vim.treesitter.query.get(
|
||||
opts.query_lang,
|
||||
opts.query_name or "luasnip"
|
||||
)
|
||||
end
|
||||
|
||||
match_opts.generator = ts.captures_iter(opts.match_captures or "prefix")
|
||||
|
||||
if type(opts.select) == "function" then
|
||||
match_opts.selector = opts.select
|
||||
elseif type(opts.select) == "string" then
|
||||
match_opts.selector = ts.builtin_tsnode_selectors[opts.select]
|
||||
assert(
|
||||
match_opts.selector,
|
||||
"Selector " .. opts.select .. "is not known"
|
||||
)
|
||||
else
|
||||
match_opts.selector = ts.builtin_tsnode_selectors.any
|
||||
end
|
||||
|
||||
---@param parser LuaSnip.extra.TSParser
|
||||
---@param pos { [1]: number, [2]: number }
|
||||
return function(parser, pos)
|
||||
return parser:match_at(
|
||||
match_opts, --[[@as LuaSnip.extra.MatchTSNodeOpts]]
|
||||
pos
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local function make_reparse_enter_and_leave_func(
|
||||
reparse,
|
||||
bufnr,
|
||||
trigger_region,
|
||||
trigger
|
||||
)
|
||||
if reparse == "live" then
|
||||
local context = ts.FixBufferContext.new(bufnr, trigger_region, trigger)
|
||||
return function()
|
||||
return context:enter()
|
||||
end, function(_)
|
||||
context:leave()
|
||||
end
|
||||
elseif reparse == "copy" then
|
||||
local parser, source =
|
||||
ts.reparse_buffer_after_removing_match(bufnr, trigger_region)
|
||||
return function()
|
||||
return parser, source
|
||||
end, function()
|
||||
parser:destroy()
|
||||
end
|
||||
else
|
||||
return function()
|
||||
return vim.treesitter.get_parser(bufnr), bufnr
|
||||
end, function(_) end
|
||||
end
|
||||
end
|
||||
|
||||
---Optionally parse the buffer
|
||||
---@param reparse boolean|string|nil
|
||||
---@param real_resolver function
|
||||
---@return fun(snippet, line_to_cursor, matched_trigger, captures):table?
|
||||
local function wrap_with_reparse_context(reparse, real_resolver)
|
||||
return function(snippet, line_to_cursor, matched_trigger, captures)
|
||||
local bufnr = vim.api.nvim_win_get_buf(0)
|
||||
local cursor = util.get_cursor_0ind()
|
||||
local trigger_region = {
|
||||
row = cursor[1],
|
||||
col_range = {
|
||||
-- includes from, excludes to.
|
||||
cursor[2] - #matched_trigger,
|
||||
cursor[2],
|
||||
},
|
||||
}
|
||||
|
||||
local enter, leave = make_reparse_enter_and_leave_func(
|
||||
reparse,
|
||||
bufnr,
|
||||
trigger_region,
|
||||
matched_trigger
|
||||
)
|
||||
local parser, source = enter()
|
||||
if parser == nil or source == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local ret = real_resolver(
|
||||
snippet,
|
||||
line_to_cursor,
|
||||
matched_trigger,
|
||||
captures,
|
||||
parser,
|
||||
source,
|
||||
bufnr,
|
||||
{ cursor[1], cursor[2] - #matched_trigger }
|
||||
)
|
||||
|
||||
leave()
|
||||
|
||||
return ret
|
||||
end
|
||||
end
|
||||
|
||||
---@param match_tsnode LuaSnip.extra.MatchTSNodeFunc Determines the constraints on the matched node.
|
||||
local function generate_resolve_expand_param(match_tsnode, user_resolver)
|
||||
---@param snippet any
|
||||
---@param line_to_cursor string
|
||||
---@param matched_trigger string
|
||||
---@param captures any
|
||||
---@param parser LanguageTree
|
||||
---@param source number|string
|
||||
---@param bufnr number
|
||||
---@param pos { [1]: number, [2]: number }
|
||||
return function(
|
||||
snippet,
|
||||
line_to_cursor,
|
||||
matched_trigger,
|
||||
captures,
|
||||
parser,
|
||||
source,
|
||||
bufnr,
|
||||
pos
|
||||
)
|
||||
local ts_parser = ts.TSParser.new(bufnr, parser, source)
|
||||
if ts_parser == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local row, col = unpack(pos)
|
||||
|
||||
local best_match, prefix_node = match_tsnode(ts_parser, { row, col })
|
||||
|
||||
if best_match == nil or prefix_node == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local start_row, start_col, _, _ = prefix_node:range()
|
||||
|
||||
local env = {
|
||||
LS_TSMATCH = vim.split(ts_parser:get_node_text(prefix_node), "\n"),
|
||||
-- filled subsequently.
|
||||
LS_TSDATA = {},
|
||||
}
|
||||
for capture_name, node in pairs(best_match) do
|
||||
env["LS_TSCAPTURE_" .. capture_name:upper()] =
|
||||
vim.split(ts_parser:get_node_text(node), "\n")
|
||||
|
||||
local from_r, from_c, to_r, to_c = node:range()
|
||||
env.LS_TSDATA[capture_name] = {
|
||||
type = node:type(),
|
||||
range = { { from_r, from_c }, { to_r, to_c } },
|
||||
}
|
||||
end
|
||||
|
||||
local ret = {
|
||||
trigger = matched_trigger,
|
||||
captures = captures,
|
||||
clear_region = {
|
||||
from = {
|
||||
start_row,
|
||||
start_col,
|
||||
},
|
||||
to = {
|
||||
pos[1],
|
||||
pos[2] + #matched_trigger,
|
||||
},
|
||||
},
|
||||
env_override = env,
|
||||
}
|
||||
|
||||
if user_resolver then
|
||||
local user_res = user_resolver(
|
||||
snippet,
|
||||
line_to_cursor,
|
||||
matched_trigger,
|
||||
captures
|
||||
)
|
||||
if user_res then
|
||||
ret = vim.tbl_deep_extend(
|
||||
"force",
|
||||
ret,
|
||||
user_res,
|
||||
{ env_override = {} }
|
||||
)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
end
|
||||
|
||||
local function generate_simple_parent_lookup_function(lookup_fun)
|
||||
---@param types string|string[]
|
||||
---@return LuaSnip.extra.MatchTSNodeFunc
|
||||
return function(types)
|
||||
local type_checker = tbl.list_to_set(types)
|
||||
---@param parser LuaSnip.extra.TSParser
|
||||
---@param pos { [1]: number, [2]: number }
|
||||
return function(parser, pos)
|
||||
-- check node just before the position.
|
||||
local root = parser:get_node_at_pos({ pos[1], pos[2] - 1 })
|
||||
|
||||
if root == nil then
|
||||
return
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
local check_node_exclude_pos = function(node)
|
||||
local _, _, end_row, end_col = node:range(false)
|
||||
return end_row == pos[1] and end_col == pos[2]
|
||||
end
|
||||
---@param node TSNode
|
||||
local check_node_type = function(node)
|
||||
return type_checker[node:type()]
|
||||
end
|
||||
|
||||
local prefix_node = lookup_fun(root, function(node)
|
||||
return check_node_type(node) and check_node_exclude_pos(node)
|
||||
end)
|
||||
if prefix_node == nil then
|
||||
return nil, nil
|
||||
end
|
||||
return {}, prefix_node
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param n number
|
||||
local function find_nth_parent(n)
|
||||
---@param parser LuaSnip.extra.TSParser
|
||||
---@param pos { [1]: number, [2]: number }
|
||||
return function(parser, pos)
|
||||
local inner_node = parser:get_node_at_pos({ pos[1], pos[2] - 1 })
|
||||
|
||||
if inner_node == nil then
|
||||
return
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
local check_node_exclude_pos = function(node)
|
||||
local _, _, end_row, end_col = node:range(false)
|
||||
return end_row == pos[1] and end_col == pos[2]
|
||||
end
|
||||
|
||||
return {}, ts.find_nth_parent(inner_node, n, check_node_exclude_pos)
|
||||
end
|
||||
end
|
||||
|
||||
local function treesitter_postfix(context, nodes, opts)
|
||||
opts = opts or {}
|
||||
vim.validate({
|
||||
context = { context, { "string", "table" } },
|
||||
nodes = { nodes, "table" },
|
||||
opts = { opts, "table" },
|
||||
})
|
||||
|
||||
context = node_util.wrap_context(context)
|
||||
context.wordTrig = false
|
||||
|
||||
---@type LuaSnip.extra.MatchTSNodeFunc
|
||||
local match_tsnode_func
|
||||
if type(context.matchTSNode) == "function" then
|
||||
match_tsnode_func = context.matchTSNode
|
||||
else
|
||||
match_tsnode_func = generate_match_tsnode_func(context.matchTSNode)
|
||||
end
|
||||
|
||||
local expand_params_resolver = generate_resolve_expand_param(
|
||||
match_tsnode_func,
|
||||
context.resolveExpandParams
|
||||
)
|
||||
|
||||
context.resolveExpandParams =
|
||||
wrap_with_reparse_context(context.reparseBuffer, expand_params_resolver)
|
||||
|
||||
return snip(context, nodes, opts)
|
||||
end
|
||||
|
||||
extend_decorator.register(
|
||||
treesitter_postfix,
|
||||
{ arg_indx = 1, extend = node_util.snippet_extend_context },
|
||||
{ arg_indx = 3 }
|
||||
)
|
||||
|
||||
return {
|
||||
treesitter_postfix = treesitter_postfix,
|
||||
builtin = {
|
||||
tsnode_matcher = {
|
||||
find_topmost_types = generate_simple_parent_lookup_function(
|
||||
ts.find_topmost_parent
|
||||
),
|
||||
find_first_types = generate_simple_parent_lookup_function(
|
||||
ts.find_first_parent
|
||||
),
|
||||
find_nth_parent = find_nth_parent,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
return {
|
||||
check = function()
|
||||
vim.health.start("luasnip")
|
||||
local jsregexp = require("luasnip.util.jsregexp")
|
||||
if jsregexp then
|
||||
vim.health.ok("jsregexp is installed")
|
||||
else
|
||||
vim.health.warn([[
|
||||
For Variable/Placeholder-transformations, luasnip requires
|
||||
the jsregexp library. See `:h luasnip-lsp-snippets-transformations` for advice
|
||||
]])
|
||||
end
|
||||
end,
|
||||
}
|
||||
@ -0,0 +1,896 @@
|
||||
local util = require("luasnip.util.util")
|
||||
local lazy_table = require("luasnip.util.lazy_table")
|
||||
local types = require("luasnip.util.types")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
|
||||
local session = require("luasnip.session")
|
||||
local snippet_collection = require("luasnip.session.snippet_collection")
|
||||
local Environ = require("luasnip.util.environ")
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
|
||||
local loader = require("luasnip.loaders")
|
||||
|
||||
local next_expand = nil
|
||||
local next_expand_params = nil
|
||||
local ls
|
||||
local luasnip_data_dir = vim.fn.stdpath("cache") .. "/luasnip"
|
||||
|
||||
local log = require("luasnip.util.log").new("main")
|
||||
|
||||
local function get_active_snip()
|
||||
local node = session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
if not node then
|
||||
return nil
|
||||
end
|
||||
while node.parent do
|
||||
node = node.parent
|
||||
end
|
||||
return node
|
||||
end
|
||||
|
||||
-- returns matching snippet (needs to be copied before usage!) and its expand-
|
||||
-- parameters(trigger and captures). params are returned here because there's
|
||||
-- no need to recalculate them.
|
||||
local function match_snippet(line, type)
|
||||
return snippet_collection.match_snippet(
|
||||
line,
|
||||
util.get_snippet_filetypes(),
|
||||
type
|
||||
)
|
||||
end
|
||||
|
||||
-- ft:
|
||||
-- * string: interpreted as filetype, return corresponding snippets.
|
||||
-- * nil: return snippets for all filetypes:
|
||||
-- {
|
||||
-- lua = {...},
|
||||
-- cpp = {...},
|
||||
-- ...
|
||||
-- }
|
||||
-- opts: optional args, can contain `type`, either "snippets" or "autosnippets".
|
||||
--
|
||||
-- return table, may be empty.
|
||||
local function get_snippets(ft, opts)
|
||||
opts = opts or {}
|
||||
|
||||
return snippet_collection.get_snippets(ft, opts.type or "snippets") or {}
|
||||
end
|
||||
|
||||
local function default_snip_info(snip)
|
||||
return {
|
||||
name = snip.name,
|
||||
trigger = snip.trigger,
|
||||
description = snip.description,
|
||||
wordTrig = snip.wordTrig and true or false,
|
||||
regTrig = snip.regTrig and true or false,
|
||||
}
|
||||
end
|
||||
|
||||
local function available(snip_info)
|
||||
snip_info = snip_info or default_snip_info
|
||||
|
||||
local fts = util.get_snippet_filetypes()
|
||||
local res = {}
|
||||
for _, ft in ipairs(fts) do
|
||||
res[ft] = {}
|
||||
for _, snip in ipairs(get_snippets(ft)) do
|
||||
if not snip.invalidated then
|
||||
table.insert(res[ft], snip_info(snip))
|
||||
end
|
||||
end
|
||||
for _, snip in ipairs(get_snippets(ft, { type = "autosnippets" })) do
|
||||
if not snip.invalidated then
|
||||
table.insert(res[ft], snip_info(snip))
|
||||
end
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local unlink_set_adjacent_as_current
|
||||
local function unlink_set_adjacent_as_current_no_log(snippet)
|
||||
-- prefer setting previous/outer insertNode as current node.
|
||||
local next_current =
|
||||
-- either pick i0 of snippet before, or i(-1) of next snippet.
|
||||
snippet.prev.prev or snippet:next_node()
|
||||
snippet:remove_from_jumplist()
|
||||
|
||||
if next_current then
|
||||
-- if snippet was active before, we need to now set its parent to be no
|
||||
-- longer inner_active.
|
||||
if
|
||||
snippet.parent_node == next_current and next_current.inner_active
|
||||
then
|
||||
snippet.parent_node:input_leave_children()
|
||||
else
|
||||
-- set no_move.
|
||||
local ok, err = pcall(next_current.input_enter, next_current, true)
|
||||
if not ok then
|
||||
-- this won't try to set the previously broken snippet as
|
||||
-- current, since that link is removed in
|
||||
-- `remove_from_jumplist`.
|
||||
unlink_set_adjacent_as_current(
|
||||
next_current.parent.snippet,
|
||||
"Error while setting adjacent snippet as current node: %s",
|
||||
err
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()] = next_current
|
||||
end
|
||||
function unlink_set_adjacent_as_current(snippet, reason, ...)
|
||||
log.warn("Removing snippet %s: %s", snippet.trigger, reason:format(...))
|
||||
unlink_set_adjacent_as_current_no_log(snippet)
|
||||
end
|
||||
|
||||
local function unlink_current()
|
||||
local current = session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
if not current then
|
||||
print("No active Snippet")
|
||||
return
|
||||
end
|
||||
unlink_set_adjacent_as_current_no_log(current.parent.snippet)
|
||||
end
|
||||
|
||||
-- return next active node.
|
||||
local function safe_jump_current(dir, no_move, dry_run)
|
||||
local node = session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
if not node then
|
||||
return nil
|
||||
end
|
||||
|
||||
local ok, res = pcall(node.jump_from, node, dir, no_move, dry_run)
|
||||
if ok then
|
||||
return res
|
||||
else
|
||||
local snip = node.parent.snippet
|
||||
|
||||
unlink_set_adjacent_as_current(
|
||||
snip,
|
||||
"Removing snippet `%s` due to error %s",
|
||||
snip.trigger,
|
||||
res
|
||||
)
|
||||
return session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
end
|
||||
end
|
||||
local function jump(dir)
|
||||
local current = session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
if current then
|
||||
local next_node = util.no_region_check_wrap(safe_jump_current, dir)
|
||||
if next_node == nil then
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()] = nil
|
||||
return true
|
||||
end
|
||||
if session.config.exit_roots then
|
||||
if next_node.pos == 0 and next_node.parent.parent_node == nil then
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()] = nil
|
||||
return true
|
||||
end
|
||||
end
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()] = next_node
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
local function jump_destination(dir)
|
||||
-- dry run of jump (+no_move ofc.), only retrieves destination-node.
|
||||
return safe_jump_current(dir, true, { active = {} })
|
||||
end
|
||||
|
||||
local function jumpable(dir)
|
||||
-- node is jumpable if there is a destination.
|
||||
return jump_destination(dir)
|
||||
~= session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
end
|
||||
|
||||
local function expandable()
|
||||
next_expand, next_expand_params =
|
||||
match_snippet(util.get_current_line_to_cursor(), "snippets")
|
||||
return next_expand ~= nil
|
||||
end
|
||||
|
||||
local function expand_or_jumpable()
|
||||
return expandable() or jumpable(1)
|
||||
end
|
||||
|
||||
local function in_snippet()
|
||||
-- check if the cursor on a row inside a snippet.
|
||||
local node = session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
if not node then
|
||||
return false
|
||||
end
|
||||
local snippet = node.parent.snippet
|
||||
local ok, snip_begin_pos, snip_end_pos =
|
||||
pcall(snippet.mark.pos_begin_end, snippet.mark)
|
||||
if not ok then
|
||||
-- if there was an error getting the position, the snippets text was
|
||||
-- most likely removed, resulting in messed up extmarks -> error.
|
||||
-- remove the snippet.
|
||||
unlink_set_adjacent_as_current(
|
||||
snippet,
|
||||
"Error while getting extmark-position: %s",
|
||||
snip_begin_pos
|
||||
)
|
||||
return
|
||||
end
|
||||
local pos = vim.api.nvim_win_get_cursor(0)
|
||||
if pos[1] - 1 >= snip_begin_pos[1] and pos[1] - 1 <= snip_end_pos[1] then
|
||||
return true -- cursor not on row inside snippet
|
||||
end
|
||||
end
|
||||
|
||||
local function expand_or_locally_jumpable()
|
||||
return expandable() or (in_snippet() and jumpable(1))
|
||||
end
|
||||
|
||||
local function locally_jumpable(dir)
|
||||
return in_snippet() and jumpable(dir)
|
||||
end
|
||||
|
||||
local function _jump_into_default(snippet)
|
||||
return util.no_region_check_wrap(snippet.jump_into, snippet, 1)
|
||||
end
|
||||
|
||||
-- opts.clear_region: table, keys `from` and `to`, both (0,0)-indexed.
|
||||
local function snip_expand(snippet, opts)
|
||||
local snip = snippet:copy()
|
||||
|
||||
opts = opts or {}
|
||||
opts.expand_params = opts.expand_params or {}
|
||||
-- override with current position if none given.
|
||||
opts.pos = opts.pos or util.get_cursor_0ind()
|
||||
opts.jump_into_func = opts.jump_into_func or _jump_into_default
|
||||
opts.indent = vim.F.if_nil(opts.indent, true)
|
||||
|
||||
snip.trigger = opts.expand_params.trigger or snip.trigger
|
||||
snip.captures = opts.expand_params.captures or {}
|
||||
|
||||
local info =
|
||||
{ trigger = snip.trigger, captures = snip.captures, pos = opts.pos }
|
||||
local env = Environ:new(info)
|
||||
Environ:override(env, opts.expand_params.env_override or {})
|
||||
|
||||
local pos_id = vim.api.nvim_buf_set_extmark(
|
||||
0,
|
||||
session.ns_id,
|
||||
opts.pos[1],
|
||||
opts.pos[2],
|
||||
-- track position between pos[2]-1 and pos[2].
|
||||
{ right_gravity = false }
|
||||
)
|
||||
|
||||
-- optionally clear text. Text has to be cleared befor jumping into the new
|
||||
-- snippet, as the cursor-position can end up in the wrong position (to be
|
||||
-- precise the text will be moved, the cursor will stay at the same
|
||||
-- position, which is just as bad) if text before the cursor, on the same
|
||||
-- line is cleared.
|
||||
if opts.clear_region then
|
||||
vim.api.nvim_buf_set_text(
|
||||
0,
|
||||
opts.clear_region.from[1],
|
||||
opts.clear_region.from[2],
|
||||
opts.clear_region.to[1],
|
||||
opts.clear_region.to[2],
|
||||
{ "" }
|
||||
)
|
||||
end
|
||||
|
||||
local snip_parent_node = snip:trigger_expand(
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()],
|
||||
pos_id,
|
||||
env,
|
||||
opts.indent
|
||||
)
|
||||
|
||||
-- jump_into-callback returns new active node.
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()] =
|
||||
opts.jump_into_func(snip)
|
||||
|
||||
local buf_snippet_roots =
|
||||
session.snippet_roots[vim.api.nvim_get_current_buf()]
|
||||
if not session.config.keep_roots and #buf_snippet_roots > 1 then
|
||||
-- if history is not set, and there is more than one snippet-root,
|
||||
-- remove the other one.
|
||||
-- The nice thing is: since we maintain that #buf_snippet_roots == 1
|
||||
-- whenever outside of this function, we know that if we're here, it's
|
||||
-- because this snippet was just inserted into buf_snippet_roots.
|
||||
-- Armed with this knowledge, we can just check which of the roots is
|
||||
-- this snippet, and remove the other one.
|
||||
buf_snippet_roots[buf_snippet_roots[1] == snip and 2 or 1]:remove_from_jumplist()
|
||||
end
|
||||
|
||||
-- stores original snippet, it doesn't contain any data from expansion.
|
||||
session.last_expand_snip = snippet
|
||||
session.last_expand_opts = opts
|
||||
|
||||
-- set last action for vim-repeat.
|
||||
-- will silently fail if vim-repeat isn't available.
|
||||
-- -1 to disable count.
|
||||
vim.cmd([[silent! call repeat#set("\<Plug>luasnip-expand-repeat", -1)]])
|
||||
|
||||
return snip
|
||||
end
|
||||
|
||||
---Find a snippet matching the current cursor-position.
|
||||
---@param opts table: may contain:
|
||||
--- - `jump_into_func`: passed through to `snip_expand`.
|
||||
---@return boolean: whether a snippet was expanded.
|
||||
local function expand(opts)
|
||||
local expand_params
|
||||
local snip
|
||||
-- find snip via next_expand (set from previous expandable()) or manual matching.
|
||||
if next_expand ~= nil then
|
||||
snip = next_expand
|
||||
expand_params = next_expand_params
|
||||
|
||||
next_expand = nil
|
||||
next_expand_params = nil
|
||||
else
|
||||
snip, expand_params =
|
||||
match_snippet(util.get_current_line_to_cursor(), "snippets")
|
||||
end
|
||||
if snip then
|
||||
local jump_into_func = opts and opts.jump_into_func
|
||||
|
||||
local cursor = util.get_cursor_0ind()
|
||||
|
||||
local clear_region = expand_params.clear_region
|
||||
or {
|
||||
from = {
|
||||
cursor[1],
|
||||
cursor[2] - #expand_params.trigger,
|
||||
},
|
||||
to = cursor,
|
||||
}
|
||||
|
||||
-- override snip with expanded copy.
|
||||
snip = snip_expand(snip, {
|
||||
expand_params = expand_params,
|
||||
-- clear trigger-text.
|
||||
clear_region = clear_region,
|
||||
jump_into_func = jump_into_func,
|
||||
})
|
||||
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function expand_auto()
|
||||
local snip, expand_params =
|
||||
match_snippet(util.get_current_line_to_cursor(), "autosnippets")
|
||||
if snip then
|
||||
local cursor = util.get_cursor_0ind()
|
||||
local clear_region = expand_params.clear_region
|
||||
or {
|
||||
from = {
|
||||
cursor[1],
|
||||
cursor[2] - #expand_params.trigger,
|
||||
},
|
||||
to = cursor,
|
||||
}
|
||||
snip = snip_expand(snip, {
|
||||
expand_params = expand_params,
|
||||
-- clear trigger-text.
|
||||
clear_region = clear_region,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local function expand_repeat()
|
||||
-- prevent clearing text with repeated expand.
|
||||
session.last_expand_opts.clear_region = nil
|
||||
session.last_expand_opts.pos = nil
|
||||
|
||||
snip_expand(session.last_expand_snip, session.last_expand_opts)
|
||||
end
|
||||
|
||||
-- return true and expand snippet if expandable, return false if not.
|
||||
local function expand_or_jump()
|
||||
if expand() then
|
||||
return true
|
||||
end
|
||||
if jump(1) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function lsp_expand(body, opts)
|
||||
-- expand snippet as-is.
|
||||
snip_expand(
|
||||
ls.parser.parse_snippet(
|
||||
"",
|
||||
body,
|
||||
{ trim_empty = false, dedent = false }
|
||||
),
|
||||
opts
|
||||
)
|
||||
end
|
||||
|
||||
local function choice_active()
|
||||
return session.active_choice_nodes[vim.api.nvim_get_current_buf()] ~= nil
|
||||
end
|
||||
|
||||
-- attempts to do some action on the snippet (like change_choice, set_choice),
|
||||
-- if it fails the snippet is removed and the next snippet becomes the current node.
|
||||
-- ... is passed to pcall as-is.
|
||||
local function safe_choice_action(snip, ...)
|
||||
local ok, res = pcall(...)
|
||||
if ok then
|
||||
return res
|
||||
else
|
||||
-- not very elegant, but this way we don't have a near
|
||||
-- re-implementation of unlink_current.
|
||||
unlink_set_adjacent_as_current(
|
||||
snip,
|
||||
"Removing snippet `%s` due to error %s",
|
||||
snip.trigger,
|
||||
res
|
||||
)
|
||||
return session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
end
|
||||
end
|
||||
local function change_choice(val)
|
||||
local active_choice =
|
||||
session.active_choice_nodes[vim.api.nvim_get_current_buf()]
|
||||
assert(active_choice, "No active choiceNode")
|
||||
local new_active = util.no_region_check_wrap(
|
||||
safe_choice_action,
|
||||
active_choice.parent.snippet,
|
||||
active_choice.change_choice,
|
||||
active_choice,
|
||||
val,
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
)
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()] = new_active
|
||||
end
|
||||
|
||||
local function set_choice(choice_indx)
|
||||
local active_choice =
|
||||
session.active_choice_nodes[vim.api.nvim_get_current_buf()]
|
||||
assert(active_choice, "No active choiceNode")
|
||||
local choice = active_choice.choices[choice_indx]
|
||||
assert(choice, "Invalid Choice")
|
||||
local new_active = util.no_region_check_wrap(
|
||||
safe_choice_action,
|
||||
active_choice.parent.snippet,
|
||||
active_choice.set_choice,
|
||||
active_choice,
|
||||
choice,
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
)
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()] = new_active
|
||||
end
|
||||
|
||||
local function get_current_choices()
|
||||
local active_choice =
|
||||
session.active_choice_nodes[vim.api.nvim_get_current_buf()]
|
||||
assert(active_choice, "No active choiceNode")
|
||||
|
||||
local choice_lines = {}
|
||||
|
||||
active_choice:update_static_all()
|
||||
for i, choice in ipairs(active_choice.choices) do
|
||||
choice_lines[i] = table.concat(choice:get_docstring(), "\n")
|
||||
end
|
||||
|
||||
return choice_lines
|
||||
end
|
||||
|
||||
local function active_update_dependents()
|
||||
local active = session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
-- special case for startNode, cannot focus on those (and they can't
|
||||
-- have dependents)
|
||||
-- don't update if a jump/change_choice is in progress.
|
||||
if not session.jump_active and active and active.pos > 0 then
|
||||
-- Save cursor-pos to restore later.
|
||||
local cur = util.get_cursor_0ind()
|
||||
local cur_mark = vim.api.nvim_buf_set_extmark(
|
||||
0,
|
||||
session.ns_id,
|
||||
cur[1],
|
||||
cur[2],
|
||||
{ right_gravity = false }
|
||||
)
|
||||
|
||||
local ok, err = pcall(active.update_dependents, active)
|
||||
if not ok then
|
||||
unlink_set_adjacent_as_current(
|
||||
active.parent.snippet,
|
||||
"Error while updating dependents for snippet %s due to error %s",
|
||||
active.parent.snippet.trigger,
|
||||
err
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
-- 'restore' orientation of extmarks, may have been changed by some set_text or similar.
|
||||
ok, err = pcall(active.focus, active)
|
||||
if not ok then
|
||||
unlink_set_adjacent_as_current(
|
||||
active.parent.snippet,
|
||||
"Error while entering node in snippet %s: %s",
|
||||
active.parent.snippet.trigger,
|
||||
err
|
||||
)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
-- Don't account for utf, nvim_win_set_cursor doesn't either.
|
||||
cur = vim.api.nvim_buf_get_extmark_by_id(
|
||||
0,
|
||||
session.ns_id,
|
||||
cur_mark,
|
||||
{ details = false }
|
||||
)
|
||||
util.set_cursor_0ind(cur)
|
||||
end
|
||||
end
|
||||
|
||||
local function store_snippet_docstrings(snippet_table)
|
||||
-- ensure the directory exists.
|
||||
-- 493 = 0755
|
||||
vim.loop.fs_mkdir(luasnip_data_dir, 493)
|
||||
|
||||
-- fs_open() with w+ creates the file if nonexistent.
|
||||
local docstring_cache_fd = vim.loop.fs_open(
|
||||
luasnip_data_dir .. "/docstrings.json",
|
||||
"w+",
|
||||
-- 420 = 0644
|
||||
420
|
||||
)
|
||||
|
||||
-- get size for fs_read()
|
||||
local cache_size = vim.loop.fs_fstat(docstring_cache_fd).size
|
||||
local file_could_be_read, docstrings = pcall(
|
||||
util.json_decode,
|
||||
-- offset 0.
|
||||
vim.loop.fs_read(docstring_cache_fd, cache_size, 0)
|
||||
)
|
||||
docstrings = file_could_be_read and docstrings or {}
|
||||
|
||||
for ft, snippets in pairs(snippet_table) do
|
||||
if not docstrings[ft] then
|
||||
docstrings[ft] = {}
|
||||
end
|
||||
for _, snippet in ipairs(snippets) do
|
||||
docstrings[ft][snippet.trigger] = snippet:get_docstring()
|
||||
end
|
||||
end
|
||||
|
||||
vim.loop.fs_write(docstring_cache_fd, util.json_encode(docstrings))
|
||||
end
|
||||
|
||||
local function load_snippet_docstrings(snippet_table)
|
||||
-- ensure the directory exists.
|
||||
-- 493 = 0755
|
||||
vim.loop.fs_mkdir(luasnip_data_dir, 493)
|
||||
|
||||
-- fs_open() with "r" returns nil if the file doesn't exist.
|
||||
local docstring_cache_fd = vim.loop.fs_open(
|
||||
luasnip_data_dir .. "/docstrings.json",
|
||||
"r",
|
||||
-- 420 = 0644
|
||||
420
|
||||
)
|
||||
|
||||
if not docstring_cache_fd then
|
||||
error("Cached docstrings could not be read!")
|
||||
return
|
||||
end
|
||||
-- get size for fs_read()
|
||||
local cache_size = vim.loop.fs_fstat(docstring_cache_fd).size
|
||||
local docstrings = util.json_decode(
|
||||
-- offset 0.
|
||||
vim.loop.fs_read(docstring_cache_fd, cache_size, 0)
|
||||
)
|
||||
|
||||
for ft, snippets in pairs(snippet_table) do
|
||||
-- skip if fieltype not in cache.
|
||||
if docstrings[ft] then
|
||||
for _, snippet in ipairs(snippets) do
|
||||
-- only set if it hasn't been set already.
|
||||
if not snippet.docstring then
|
||||
snippet.docstring = docstrings[ft][snippet.trigger]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function unlink_current_if_deleted()
|
||||
local node = session.current_nodes[vim.api.nvim_get_current_buf()]
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
local snippet = node.parent.snippet
|
||||
|
||||
-- extmarks_valid checks that
|
||||
-- * textnodes that should contain text still do so, and
|
||||
-- * that extmarks still fulfill all expectations (should be successive, no gaps, etc.)
|
||||
if not snippet:extmarks_valid() then
|
||||
unlink_set_adjacent_as_current(
|
||||
snippet,
|
||||
"Detected deletion of snippet `%s`, removing it",
|
||||
snippet.trigger
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local function exit_out_of_region(node)
|
||||
-- if currently jumping via luasnip or no active node:
|
||||
if session.jump_active or not node then
|
||||
return
|
||||
end
|
||||
|
||||
local pos = util.get_cursor_0ind()
|
||||
local snippet
|
||||
if node.type == types.snippet then
|
||||
snippet = node
|
||||
else
|
||||
snippet = node.parent.snippet
|
||||
end
|
||||
|
||||
-- find root-snippet.
|
||||
while snippet.parent_node do
|
||||
snippet = snippet.parent_node.parent.snippet
|
||||
end
|
||||
|
||||
local ok, snip_begin_pos, snip_end_pos =
|
||||
pcall(snippet.mark.pos_begin_end, snippet.mark)
|
||||
|
||||
if not ok then
|
||||
unlink_set_adjacent_as_current(
|
||||
snippet,
|
||||
"Error while getting extmark-position: %s",
|
||||
snip_begin_pos
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
-- stylua: ignore
|
||||
-- leave if curser before or behind snippet
|
||||
if pos[1] < snip_begin_pos[1] or
|
||||
pos[1] > snip_end_pos[1] then
|
||||
|
||||
-- make sure the snippet can safely be entered, since it may have to
|
||||
-- be, in `refocus`.
|
||||
if not snippet:extmarks_valid() then
|
||||
unlink_set_adjacent_as_current(snippet, "Leaving snippet-root due to invalid extmarks.")
|
||||
return
|
||||
end
|
||||
|
||||
local next_active = snippet.insert_nodes[0]
|
||||
-- if there is a snippet nested into the $0, enter its $0 instead,
|
||||
-- recursively.
|
||||
-- This is to ensure that a jump forward after leaving the region of a
|
||||
-- root will jump to the next root, or not result in a jump at all.
|
||||
while next_active.inner_first do
|
||||
-- make sure next_active is nested into completely intact
|
||||
-- snippets, since that is a precondition on the to-node of
|
||||
if not next_active.inner_first:extmarks_valid() then
|
||||
next_active.inner_first:remove_from_jumplist()
|
||||
else
|
||||
-- inner_first is always the snippet, not the -1-node.
|
||||
next_active = next_active.inner_first.insert_nodes[0]
|
||||
end
|
||||
end
|
||||
|
||||
node_util.refocus(node, next_active)
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()] = next_active
|
||||
end
|
||||
end
|
||||
|
||||
-- ft string, extend_ft table of strings.
|
||||
local function filetype_extend(ft, extend_ft)
|
||||
vim.list_extend(session.ft_redirect[ft], extend_ft)
|
||||
session.ft_redirect[ft] = util.deduplicate(session.ft_redirect[ft])
|
||||
end
|
||||
|
||||
-- ft string, fts table of strings.
|
||||
local function filetype_set(ft, fts)
|
||||
session.ft_redirect[ft] = util.deduplicate(fts)
|
||||
end
|
||||
|
||||
local function cleanup()
|
||||
-- Use this to reload luasnip
|
||||
vim.api.nvim_exec_autocmds(
|
||||
"User",
|
||||
{ pattern = "LuasnipCleanup", modeline = false }
|
||||
)
|
||||
-- clear all snippets.
|
||||
snippet_collection.clear_snippets()
|
||||
loader.cleanup()
|
||||
end
|
||||
|
||||
local function refresh_notify(ft)
|
||||
snippet_collection.refresh_notify(ft)
|
||||
end
|
||||
|
||||
local function setup_snip_env()
|
||||
local combined_table = vim.tbl_extend("force", _G, session.config.snip_env)
|
||||
-- TODO: if desired, take into account _G's __index before looking into
|
||||
-- snip_env's __index.
|
||||
setmetatable(combined_table, getmetatable(session.config.snip_env))
|
||||
|
||||
setfenv(2, combined_table)
|
||||
end
|
||||
local function get_snip_env()
|
||||
return session.get_snip_env()
|
||||
end
|
||||
|
||||
local function get_id_snippet(id)
|
||||
return snippet_collection.get_id_snippet(id)
|
||||
end
|
||||
|
||||
local function add_snippets(ft, snippets, opts)
|
||||
-- don't use yet, not available in some neovim-versions.
|
||||
--
|
||||
-- vim.validate({
|
||||
-- filetype = { ft, { "string", "nil" } },
|
||||
-- snippets = { snippets, "table" },
|
||||
-- opts = { opts, { "table", "nil" } },
|
||||
-- })
|
||||
|
||||
opts = opts or {}
|
||||
opts.refresh_notify = opts.refresh_notify or true
|
||||
-- alternatively, "autosnippets"
|
||||
opts.type = opts.type or "snippets"
|
||||
|
||||
-- if ft is nil, snippets already has this format.
|
||||
if ft then
|
||||
snippets = {
|
||||
[ft] = snippets,
|
||||
}
|
||||
end
|
||||
|
||||
snippet_collection.add_snippets(snippets, opts)
|
||||
|
||||
if opts.refresh_notify then
|
||||
for ft_, _ in pairs(snippets) do
|
||||
refresh_notify(ft_)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function clean_invalidated(opts)
|
||||
opts = opts or {}
|
||||
snippet_collection.clean_invalidated(opts)
|
||||
end
|
||||
|
||||
local function activate_node(opts)
|
||||
opts = opts or {}
|
||||
local pos = opts.pos or util.get_cursor_0ind()
|
||||
local strict = vim.F.if_nil(opts.strict, false)
|
||||
local select = vim.F.if_nil(opts.select, true)
|
||||
|
||||
-- find tree-node the snippet should be inserted at (could be before another node).
|
||||
local _, _, _, node = node_util.snippettree_find_undamaged_node(pos, {
|
||||
tree_respect_rgravs = false,
|
||||
tree_preference = node_util.binarysearch_preference.inside,
|
||||
snippet_mode = "interactive",
|
||||
})
|
||||
|
||||
if not node then
|
||||
error("No Snippet at that position")
|
||||
return
|
||||
end
|
||||
|
||||
-- only activate interactive nodes, or nodes that are immediately nested
|
||||
-- inside a choiceNode.
|
||||
if not node:interactive() then
|
||||
if strict then
|
||||
error("Refusing to activate a non-interactive node.")
|
||||
return
|
||||
else
|
||||
-- fall back to known insertNode.
|
||||
-- snippet.insert_nodes[1] may be preferable, but that is not
|
||||
-- certainly an insertNode (and does not even certainly contain an
|
||||
-- insertNode, think snippetNode with only textNode).
|
||||
-- We could *almost* find the first activateable node by
|
||||
-- dry_run-jumping into the snippet, but then we'd also need some
|
||||
-- mechanism for setting the active-state of all nodes to false,
|
||||
-- which we don't yet have.
|
||||
--
|
||||
-- Instead, just choose -1-node, and allow jumps from there, which
|
||||
-- is much simpler.
|
||||
node = node.parent.snippet.prev
|
||||
end
|
||||
end
|
||||
|
||||
node_util.refocus(
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()],
|
||||
node
|
||||
)
|
||||
if select then
|
||||
-- input_enter node again, to get highlight and the like.
|
||||
-- One side-effect of this is that an event will be execute twice, but I
|
||||
-- feel like that is a trade-off worth doing, since it otherwise refocus
|
||||
-- would have to be more complicated (or at least, restructured).
|
||||
node:input_enter()
|
||||
end
|
||||
session.current_nodes[vim.api.nvim_get_current_buf()] = node
|
||||
end
|
||||
|
||||
-- make these lazy, such that we don't have to load them before it's really
|
||||
-- necessary (drives up cost of initial load, otherwise).
|
||||
-- stylua: ignore
|
||||
local ls_lazy = {
|
||||
s = function() return require("luasnip.nodes.snippet").S end,
|
||||
sn = function() return require("luasnip.nodes.snippet").SN end,
|
||||
t = function() return require("luasnip.nodes.textNode").T end,
|
||||
f = function() return require("luasnip.nodes.functionNode").F end,
|
||||
i = function() return require("luasnip.nodes.insertNode").I end,
|
||||
c = function() return require("luasnip.nodes.choiceNode").C end,
|
||||
d = function() return require("luasnip.nodes.dynamicNode").D end,
|
||||
r = function() return require("luasnip.nodes.restoreNode").R end,
|
||||
snippet = function() return require("luasnip.nodes.snippet").S end,
|
||||
snippet_node = function() return require("luasnip.nodes.snippet").SN end,
|
||||
parent_indexer = function() return require("luasnip.nodes.snippet").P end,
|
||||
indent_snippet_node = function() return require("luasnip.nodes.snippet").ISN end,
|
||||
text_node = function() return require("luasnip.nodes.textNode").T end,
|
||||
function_node = function() return require("luasnip.nodes.functionNode").F end,
|
||||
insert_node = function() return require("luasnip.nodes.insertNode").I end,
|
||||
choice_node = function() return require("luasnip.nodes.choiceNode").C end,
|
||||
dynamic_node = function() return require("luasnip.nodes.dynamicNode").D end,
|
||||
restore_node = function() return require("luasnip.nodes.restoreNode").R end,
|
||||
parser = function() return require("luasnip.util.parser") end,
|
||||
config = function() return require("luasnip.config") end,
|
||||
multi_snippet = function() return require("luasnip.nodes.multiSnippet").new_multisnippet end,
|
||||
snippet_source = function() return require("luasnip.session.snippet_collection.source") end,
|
||||
select_keys = function() return require("luasnip.util.select").select_keys end
|
||||
}
|
||||
|
||||
ls = lazy_table({
|
||||
expand_or_jumpable = expand_or_jumpable,
|
||||
expand_or_locally_jumpable = expand_or_locally_jumpable,
|
||||
locally_jumpable = locally_jumpable,
|
||||
jumpable = jumpable,
|
||||
expandable = expandable,
|
||||
in_snippet = in_snippet,
|
||||
expand = expand,
|
||||
snip_expand = snip_expand,
|
||||
expand_repeat = expand_repeat,
|
||||
expand_auto = expand_auto,
|
||||
expand_or_jump = expand_or_jump,
|
||||
jump = jump,
|
||||
get_active_snip = get_active_snip,
|
||||
choice_active = choice_active,
|
||||
change_choice = change_choice,
|
||||
set_choice = set_choice,
|
||||
get_current_choices = get_current_choices,
|
||||
unlink_current = unlink_current,
|
||||
lsp_expand = lsp_expand,
|
||||
active_update_dependents = active_update_dependents,
|
||||
available = available,
|
||||
exit_out_of_region = exit_out_of_region,
|
||||
load_snippet_docstrings = load_snippet_docstrings,
|
||||
store_snippet_docstrings = store_snippet_docstrings,
|
||||
unlink_current_if_deleted = unlink_current_if_deleted,
|
||||
filetype_extend = filetype_extend,
|
||||
filetype_set = filetype_set,
|
||||
add_snippets = add_snippets,
|
||||
get_snippets = get_snippets,
|
||||
get_id_snippet = get_id_snippet,
|
||||
setup_snip_env = setup_snip_env,
|
||||
get_snip_env = get_snip_env,
|
||||
clean_invalidated = clean_invalidated,
|
||||
get_snippet_filetypes = util.get_snippet_filetypes,
|
||||
jump_destination = jump_destination,
|
||||
session = session,
|
||||
cleanup = cleanup,
|
||||
refresh_notify = refresh_notify,
|
||||
env_namespace = Environ.env_namespace,
|
||||
setup = require("luasnip.config").setup,
|
||||
extend_decorator = extend_decorator,
|
||||
log = require("luasnip.util.log"),
|
||||
activate_node = activate_node,
|
||||
}, ls_lazy)
|
||||
|
||||
return ls
|
||||
@ -0,0 +1,27 @@
|
||||
--- This module stores all files loaded by any of the loaders, ordered by their
|
||||
--- filetype, and other data.
|
||||
--- This is to facilitate luasnip.loaders.edit_snippets, and to handle
|
||||
--- persistency of data, which is not given if it is stored in the module-file,
|
||||
--- since the module-name we use (luasnip.loaders.*) is not necessarily the one
|
||||
--- used by the user (luasnip/loader/*, for example), and the returned modules
|
||||
--- are different tables.
|
||||
|
||||
local autotable = require("luasnip.util.auto_table").autotable
|
||||
|
||||
local M = {
|
||||
lua_collections = {},
|
||||
lua_ft_paths = autotable(2),
|
||||
|
||||
snipmate_collections = {},
|
||||
snipmate_ft_paths = autotable(2),
|
||||
-- set by loader.
|
||||
snipmate_cache = nil,
|
||||
|
||||
vscode_package_collections = {},
|
||||
vscode_standalone_watchers = {},
|
||||
vscode_ft_paths = autotable(2),
|
||||
-- set by loader.
|
||||
vscode_cache = nil,
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,466 @@
|
||||
-- loads snippets from directory structured almost like snipmate-collection:
|
||||
-- - files all named <ft>.lua
|
||||
-- - each returns table containing keys (optional) "snippets" and
|
||||
-- "autosnippets", value for each a list of snippets.
|
||||
--
|
||||
-- cache:
|
||||
-- - lazy_load_paths: {
|
||||
-- {
|
||||
-- add_opts = {...},
|
||||
-- ft1 = {filename1, filename2},
|
||||
-- ft2 = {filename1},
|
||||
-- ...
|
||||
-- }, {
|
||||
-- add_opts = {...},
|
||||
-- ft1 = {filename1},
|
||||
-- ...
|
||||
-- }
|
||||
-- }
|
||||
--
|
||||
-- each call to load generates a new entry in that list. We cannot just merge
|
||||
-- all files for some ft since add_opts might be different (they might be from
|
||||
-- different lazy_load-calls).
|
||||
|
||||
local loader_util = require("luasnip.loaders.util")
|
||||
local log = require("luasnip.util.log").new("lua-loader")
|
||||
local session = require("luasnip.session")
|
||||
local util = require("luasnip.util.util")
|
||||
local autotable = require("luasnip.util.auto_table").autotable
|
||||
local tree_watcher = require("luasnip.loaders.fs_watchers").tree
|
||||
local path_watcher = require("luasnip.loaders.fs_watchers").path
|
||||
local digraph = require("luasnip.util.directed_graph")
|
||||
local refresh_notify =
|
||||
require("luasnip.session.enqueueable_operations").refresh_notify
|
||||
local clean_invalidated =
|
||||
require("luasnip.session.enqueueable_operations").clean_invalidated
|
||||
|
||||
local Data = require("luasnip.loaders.data")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- ASSUMPTION: this function will only be called inside the snippet-constructor,
|
||||
-- to find the location of the lua-loaded file calling it.
|
||||
-- It is not exported, because it will (in its current state) only ever be used
|
||||
-- in one place, and it feels a bit wrong to expose put a function into `M`.
|
||||
-- Instead, it is inserted into the global environment before a luasnippet-file
|
||||
-- is loaded, and removed from it immediately when this is done
|
||||
local function get_loaded_file_debuginfo()
|
||||
-- we can skip looking at the first four stackframes, since
|
||||
-- 1 is this function
|
||||
-- 2 is the snippet-constructor
|
||||
-- ... (here anything is going on, could be 0 stackframes, could be many)
|
||||
-- n-2 (at least 3) is the loaded file
|
||||
-- n-1 (at least 4) is pcall
|
||||
-- n (at least 5) is _luasnip_load_file
|
||||
local current_call_depth = 4
|
||||
local debuginfo
|
||||
|
||||
repeat
|
||||
current_call_depth = current_call_depth + 1
|
||||
debuginfo = debug.getinfo(current_call_depth, "n")
|
||||
until debuginfo.name == "_luasnip_load_file"
|
||||
|
||||
-- ret is stored into a local, and not returned immediately to prevent tail
|
||||
-- call optimization, which seems to invalidate the stackframe-numbers
|
||||
-- determined earlier.
|
||||
--
|
||||
-- current_call_depth-0 is _luasnip_load_file,
|
||||
-- current_call_depth-1 is pcall, and
|
||||
-- current_call_depth-2 is the lua-loaded file.
|
||||
-- "Sl": get only source-file and current line.
|
||||
local ret = debug.getinfo(current_call_depth - 2, "Sl")
|
||||
return ret
|
||||
end
|
||||
|
||||
local function search_lua_rtp(modulename)
|
||||
-- essentially stolen from vim.loader.
|
||||
local rtp_lua_path = package.path
|
||||
for _, path in ipairs(vim.api.nvim_get_runtime_file("", true)) do
|
||||
rtp_lua_path = rtp_lua_path
|
||||
.. (";%s/lua/?.lua;%s/lua/?/init.lua"):format(path, path)
|
||||
end
|
||||
|
||||
return package.searchpath(modulename, rtp_lua_path)
|
||||
end
|
||||
|
||||
local function _luasnip_load_file(file)
|
||||
-- vim.loader.enabled does not seem to be official api, so always reset
|
||||
-- if the loader is available.
|
||||
-- To be sure, even pcall it, in case there are conditions under which
|
||||
-- it might error.
|
||||
if vim.loader then
|
||||
-- pcall, not sure if this can fail in some way..
|
||||
-- Does not seem like it though
|
||||
local ok, res = pcall(vim.loader.reset, file)
|
||||
if not ok then
|
||||
log.warn("Could not reset cache for file %s\n: %s", file, res)
|
||||
end
|
||||
end
|
||||
|
||||
local func, error_msg = loadfile(file)
|
||||
if error_msg then
|
||||
log.error("Failed to load %s\n: %s", file, error_msg)
|
||||
error(string.format("Failed to load %s\n: %s", file, error_msg))
|
||||
end
|
||||
|
||||
-- the loaded file may add snippets to these tables, they'll be
|
||||
-- combined with the snippets returned regularly.
|
||||
local file_added_snippets = {}
|
||||
local file_added_autosnippets = {}
|
||||
|
||||
local dependent_files = {}
|
||||
|
||||
-- setup snip_env in func
|
||||
local func_env
|
||||
local function ls_tracked_dofile(filename)
|
||||
local package_func, err_msg = loadfile(filename)
|
||||
if package_func then
|
||||
setfenv(package_func, func_env)
|
||||
table.insert(dependent_files, filename)
|
||||
else
|
||||
error(("File %s could not be loaded: %s"):format(filename, err_msg))
|
||||
end
|
||||
|
||||
return package_func()
|
||||
end
|
||||
func_env = vim.tbl_extend(
|
||||
"force",
|
||||
-- extend the current(expected!) globals with the snip_env, and the
|
||||
-- two tables.
|
||||
_G,
|
||||
session.get_snip_env(),
|
||||
{
|
||||
ls_file_snippets = file_added_snippets,
|
||||
ls_file_autosnippets = file_added_autosnippets,
|
||||
ls_tracked_dofile = ls_tracked_dofile,
|
||||
ls_tracked_dopackage = function(package_name)
|
||||
local package_file = search_lua_rtp(package_name)
|
||||
if not package_file then
|
||||
error(
|
||||
("Could not find package %s in rtp and package.path"):format(
|
||||
package_name
|
||||
)
|
||||
)
|
||||
end
|
||||
return ls_tracked_dofile(package_file)
|
||||
end,
|
||||
}
|
||||
)
|
||||
-- defaults snip-env requires metatable for resolving
|
||||
-- lazily-initialized keys. If we have to combine this with an eventual
|
||||
-- metatable of _G, look into unifying ls.setup_snip_env and this.
|
||||
setmetatable(func_env, getmetatable(session.get_snip_env()))
|
||||
setfenv(func, func_env)
|
||||
|
||||
-- Since this function has to reach the snippet-constructor, and fenvs
|
||||
-- aren't inherited by called functions, we have to set it in the global
|
||||
-- environment.
|
||||
_G.__luasnip_get_loaded_file_frame_debuginfo = util.ternary(
|
||||
session.config.loaders_store_source,
|
||||
get_loaded_file_debuginfo,
|
||||
nil
|
||||
)
|
||||
local run_ok, file_snippets, file_autosnippets = pcall(func)
|
||||
-- immediately nil it.
|
||||
_G.__luasnip_get_loaded_file_frame_debuginfo = nil
|
||||
|
||||
if not run_ok then
|
||||
log.error("Failed to execute\n: %s", file, file_snippets)
|
||||
error("Failed to execute " .. file .. "\n: " .. file_snippets)
|
||||
end
|
||||
|
||||
-- make sure these aren't nil.
|
||||
file_snippets = file_snippets or {}
|
||||
file_autosnippets = file_autosnippets or {}
|
||||
|
||||
vim.list_extend(file_snippets, file_added_snippets)
|
||||
vim.list_extend(file_autosnippets, file_added_autosnippets)
|
||||
|
||||
return file_snippets, file_autosnippets, dependent_files
|
||||
end
|
||||
|
||||
local function lua_package_file_filter(fname)
|
||||
return fname:match("%.lua$")
|
||||
end
|
||||
|
||||
--- Collection watches all files that belong to a collection of snippets below
|
||||
--- some root, and registers new files.
|
||||
local Collection = {}
|
||||
local Collection_mt = {
|
||||
__index = Collection,
|
||||
}
|
||||
|
||||
function Collection.new(
|
||||
root,
|
||||
lazy,
|
||||
include_ft,
|
||||
exclude_ft,
|
||||
add_opts,
|
||||
lazy_watcher,
|
||||
fs_event_providers
|
||||
)
|
||||
local ft_filter = loader_util.ft_filter(include_ft, exclude_ft)
|
||||
local o = setmetatable({
|
||||
root = root,
|
||||
file_filter = function(path, ft)
|
||||
if not path:sub(1, #root) == root then
|
||||
log.warn(
|
||||
"Tried to filter file `%s`, which is not inside the root `%s`.",
|
||||
path,
|
||||
root
|
||||
)
|
||||
return false
|
||||
end
|
||||
return lua_package_file_filter(path) and ft_filter(ft)
|
||||
end,
|
||||
add_opts = add_opts,
|
||||
lazy = lazy,
|
||||
-- store ft -> set of files that should be lazy-loaded.
|
||||
lazy_files = autotable(2, { warn = false }),
|
||||
-- store, for all files in this collection, their filetype.
|
||||
-- No need to always recompute it, and we can use this to store which
|
||||
-- files belong to the collection.
|
||||
loaded_path_ft = {},
|
||||
file_dependencies = digraph.new_labeled(),
|
||||
-- store fs_watchers for files the snippets-files depend on.
|
||||
dependency_watchers = {},
|
||||
fs_event_providers = fs_event_providers,
|
||||
}, Collection_mt)
|
||||
|
||||
-- only register files up to a depth of 2.
|
||||
local ok, err_or_watcher = pcall(tree_watcher, root, 2, {
|
||||
-- don't handle removals for now.
|
||||
new_file = function(path)
|
||||
local path_ft = loader_util.collection_file_ft(o.root, path)
|
||||
-- detected new file, make sure it is allowed by our filters.
|
||||
if o.file_filter(path, path_ft) then
|
||||
o:add_file(path, path_ft)
|
||||
end
|
||||
end,
|
||||
change_file = function(path)
|
||||
o:reload(path)
|
||||
end,
|
||||
}, { lazy = lazy_watcher, fs_event_providers = fs_event_providers })
|
||||
|
||||
if not ok then
|
||||
error(("Could not create watcher: %s"):format(err_or_watcher))
|
||||
end
|
||||
|
||||
o.watcher = err_or_watcher
|
||||
|
||||
log.info("Initialized snippet-collection at `%s`", root)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
-- Add file with some filetype to collection.
|
||||
function Collection:add_file(path, ft)
|
||||
Data.lua_ft_paths[ft][path] = true
|
||||
|
||||
if self.lazy then
|
||||
if not session.loaded_fts[ft] then
|
||||
log.info(
|
||||
"Registering lazy-load-snippets for ft `%s` from file `%s`",
|
||||
ft,
|
||||
path
|
||||
)
|
||||
|
||||
-- only register to load later.
|
||||
self.lazy_files[ft][path] = true
|
||||
return
|
||||
else
|
||||
log.info(
|
||||
"Filetype `%s` is already active, loading immediately.",
|
||||
ft
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
self:load_file(path, ft)
|
||||
end
|
||||
function Collection:load_file(path, ft)
|
||||
log.info("Adding snippets for filetype `%s` from file `%s`", ft, path)
|
||||
self.loaded_path_ft[path] = ft
|
||||
|
||||
local snippets, autosnippets, dependent_files = _luasnip_load_file(path)
|
||||
|
||||
-- ignored if it already exists.
|
||||
self.file_dependencies:set_vertex(path)
|
||||
-- make sure we don't retain any old dependencies.
|
||||
self.file_dependencies:clear_edges(path)
|
||||
|
||||
for _, file_dependency in ipairs(dependent_files) do
|
||||
-- ignored if it already exists.
|
||||
self.file_dependencies:set_vertex(file_dependency)
|
||||
-- path depends on dependent_file => if dependent_file is changed, path
|
||||
-- should be updated.
|
||||
self.file_dependencies:set_edge(file_dependency, path, path)
|
||||
|
||||
if not self.dependency_watchers[file_dependency] then
|
||||
self.dependency_watchers[file_dependency] = path_watcher(
|
||||
file_dependency,
|
||||
{
|
||||
change = function(_)
|
||||
local depending_files =
|
||||
self.file_dependencies:connected_component(
|
||||
file_dependency,
|
||||
"Forward"
|
||||
)
|
||||
for _, file in ipairs(depending_files) do
|
||||
-- Prevent loading one of the utility-files as a snippet-file.
|
||||
-- This will not reject any snippet-file in
|
||||
-- depending_files. This is because since they are in
|
||||
-- depending_files, we have their dependency-information,
|
||||
-- which can only be obtained by loading them, and so there
|
||||
-- can't be any unloaded files in there.
|
||||
if self.loaded_path_ft[file] then
|
||||
self:load_file(file, self.loaded_path_ft[file])
|
||||
end
|
||||
end
|
||||
end,
|
||||
},
|
||||
{ lazy = false, fs_event_providers = self.fs_event_providers }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
loader_util.add_file_snippets(
|
||||
ft,
|
||||
path,
|
||||
snippets,
|
||||
autosnippets,
|
||||
self.add_opts
|
||||
)
|
||||
|
||||
refresh_notify(ft)
|
||||
end
|
||||
function Collection:do_lazy_load(ft)
|
||||
for file, _ in pairs(self.lazy_files[ft]) do
|
||||
if not self.loaded_path_ft[file] then
|
||||
self:load_file(file, ft)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- will only do something, if the file at `path` was loaded previously.
|
||||
function Collection:reload(path)
|
||||
local path_ft = self.loaded_path_ft[path]
|
||||
if not path_ft then
|
||||
-- file not yet loaded.
|
||||
return
|
||||
end
|
||||
|
||||
-- will override previously-loaded snippets from this path.
|
||||
self:load_file(path, path_ft)
|
||||
|
||||
-- clean snippets if enough were removed.
|
||||
clean_invalidated()
|
||||
end
|
||||
|
||||
function Collection:stop()
|
||||
self.watcher:stop()
|
||||
for _, watcher in pairs(self.dependency_watchers) do
|
||||
watcher:stop()
|
||||
end
|
||||
end
|
||||
|
||||
function M._load_lazy_loaded_ft(ft)
|
||||
log.info("Loading lazy-load-snippets for filetype `%s`", ft)
|
||||
|
||||
for _, collection in ipairs(Data.lua_collections) do
|
||||
collection:do_lazy_load(ft)
|
||||
end
|
||||
end
|
||||
|
||||
local function _load(lazy, opts)
|
||||
local o = loader_util.normalize_opts(opts)
|
||||
|
||||
local collection_roots =
|
||||
loader_util.resolve_root_paths(o.paths, "luasnippets")
|
||||
local lazy_roots = loader_util.resolve_lazy_root_paths(o.lazy_paths)
|
||||
|
||||
log.info(
|
||||
"Found roots `%s` for paths `%s`.",
|
||||
vim.inspect(collection_roots),
|
||||
vim.inspect(o.paths)
|
||||
)
|
||||
if o.paths and #o.paths ~= #collection_roots then
|
||||
log.warn(
|
||||
"Could not resolve all collection-roots for paths `%s`: only found `%s`",
|
||||
vim.inspect(o.paths),
|
||||
vim.inspect(collection_roots)
|
||||
)
|
||||
end
|
||||
|
||||
log.info(
|
||||
"Determined roots `%s` for lazy_paths `%s`.",
|
||||
vim.inspect(lazy_roots),
|
||||
vim.inspect(o.lazy_paths)
|
||||
)
|
||||
if o.lazy_paths and #o.lazy_paths ~= #lazy_roots then
|
||||
log.warn(
|
||||
"Could not resolve all collection-roots for lazy_paths `%s`: only found `%s`",
|
||||
vim.inspect(o.lazy_paths),
|
||||
vim.inspect(lazy_roots)
|
||||
)
|
||||
end
|
||||
|
||||
for paths_lazy, roots in pairs({
|
||||
[true] = lazy_roots,
|
||||
[false] = collection_roots,
|
||||
}) do
|
||||
for _, collection_root in ipairs(roots) do
|
||||
local ok, coll_or_err = pcall(
|
||||
Collection.new,
|
||||
collection_root,
|
||||
lazy,
|
||||
o.include,
|
||||
o.exclude,
|
||||
o.add_opts,
|
||||
paths_lazy,
|
||||
o.fs_event_providers
|
||||
)
|
||||
|
||||
if not ok then
|
||||
log.error(
|
||||
"Could not create collection at %s: %s",
|
||||
collection_root,
|
||||
coll_or_err
|
||||
)
|
||||
else
|
||||
table.insert(Data.lua_collections, coll_or_err)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Load lua-snippet-collections immediately.
|
||||
--- @param opts LuaSnip.Loaders.LoadOpts?
|
||||
function M.load(opts)
|
||||
_load(false, opts)
|
||||
end
|
||||
|
||||
--- Load lua-snippet-collections on demand.
|
||||
--- @param opts LuaSnip.Loaders.LoadOpts?
|
||||
function M.lazy_load(opts)
|
||||
_load(true, opts)
|
||||
|
||||
-- load for current buffer on startup.
|
||||
for _, ft in
|
||||
ipairs(loader_util.get_load_fts(vim.api.nvim_get_current_buf()))
|
||||
do
|
||||
M._load_lazy_loaded_ft(ft)
|
||||
end
|
||||
end
|
||||
|
||||
function M.clean()
|
||||
for _, collection in ipairs(Data.lua_collections) do
|
||||
collection:stop()
|
||||
end
|
||||
-- bit ugly, keep in sync with defaults in data.lua.
|
||||
-- Don't anticipate those changing, so fine I guess.
|
||||
Data.lua_collections = {}
|
||||
Data.lua_ft_paths = autotable(2)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,512 @@
|
||||
local loader_util = require("luasnip.loaders.util")
|
||||
local util = require("luasnip.util.util")
|
||||
local tbl_util = require("luasnip.util.table")
|
||||
local Path = require("luasnip.util.path")
|
||||
local autotable = require("luasnip.util.auto_table").autotable
|
||||
local digraph = require("luasnip.util.directed_graph")
|
||||
local tree_watcher = require("luasnip.loaders.fs_watchers").tree
|
||||
local Data = require("luasnip.loaders.data")
|
||||
local session = require("luasnip.session")
|
||||
local snippetcache = require("luasnip.loaders.snippet_cache")
|
||||
local refresh_notify =
|
||||
require("luasnip.session.enqueueable_operations").refresh_notify
|
||||
local clean_invalidated =
|
||||
require("luasnip.session.enqueueable_operations").clean_invalidated
|
||||
|
||||
local log = require("luasnip.util.log").new("snipmate-loader")
|
||||
|
||||
--- Load data from any snippet-file.
|
||||
--- @param filename string
|
||||
--- @return LuaSnip.Loaders.SnippetFileData
|
||||
local function load_snipmate(filename)
|
||||
local buffer_ok, buffer = pcall(Path.read_file, filename)
|
||||
if not buffer_ok then
|
||||
log.error(("Could not read file %s: %s"):format(filename, buffer))
|
||||
-- return dummy-data.
|
||||
return {
|
||||
snippets = {},
|
||||
autosnippets = {},
|
||||
misc = {},
|
||||
}
|
||||
end
|
||||
|
||||
local sp = require("luasnip.nodes.snippetProxy")
|
||||
local snipmate_parse_fn = require("luasnip.util.parser").parse_snipmate
|
||||
local source = require("luasnip.session.snippet_collection.source")
|
||||
|
||||
-- could also be separate variables, but easier to access this way.
|
||||
local snippets = {
|
||||
snippet = {},
|
||||
autosnippet = {},
|
||||
}
|
||||
local extends = {}
|
||||
|
||||
---@type string[]
|
||||
local lines = loader_util.split_lines(buffer)
|
||||
local i = 1
|
||||
|
||||
local function _parse(snippet_type, snipmate_opts)
|
||||
local line = lines[i]
|
||||
-- "snippet" or "autosnippet"
|
||||
local prefix, description =
|
||||
line:match("^" .. snippet_type .. [[%s+(%S+)%s*(.*)]])
|
||||
local body = {}
|
||||
|
||||
local snip_begin_line = i
|
||||
i = i + 1
|
||||
---@type number
|
||||
local indent
|
||||
|
||||
while i <= #lines do
|
||||
line = lines[i]
|
||||
if line:find("^%s+") then
|
||||
if not indent then
|
||||
indent = #line:match("^%s+")
|
||||
end
|
||||
line = line:sub(indent + 1)
|
||||
line = line:gsub("${VISUAL}", "${TM_SELECTED_TEXT}")
|
||||
elseif line ~= "" then
|
||||
break
|
||||
end
|
||||
table.insert(body, line)
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
body = table.concat(body, "\n")
|
||||
local snip = sp(
|
||||
{
|
||||
trig = prefix,
|
||||
desc = description,
|
||||
wordTrig = true,
|
||||
priority = snipmate_opts.priority,
|
||||
},
|
||||
body,
|
||||
{
|
||||
parse_fn = snipmate_parse_fn,
|
||||
}
|
||||
)
|
||||
if session.config.loaders_store_source then
|
||||
snip._source = source.from_location(
|
||||
filename,
|
||||
{ line = snip_begin_line, line_end = i - 1 }
|
||||
)
|
||||
end
|
||||
table.insert(snippets[snippet_type], snip)
|
||||
end
|
||||
|
||||
-- options for some snippet can be specified in the lines before the
|
||||
-- {auto}snippet-keyword ("priority 2000\nsnippet....").
|
||||
-- They are stored in snipmate_opts, which is cleaned whenever a snippet is
|
||||
-- actually created.
|
||||
local snipmate_opts = {}
|
||||
while i <= #lines do
|
||||
local line = lines[i]
|
||||
if vim.startswith(line, "snippet") then
|
||||
_parse("snippet", snipmate_opts)
|
||||
snipmate_opts = {}
|
||||
elseif vim.startswith(line, "autosnippet") then
|
||||
_parse("autosnippet", snipmate_opts)
|
||||
snipmate_opts = {}
|
||||
elseif vim.startswith(line, "extends") then
|
||||
vim.list_extend(extends, vim.split(vim.trim(line:sub(8)), "[,%s]+"))
|
||||
i = i + 1
|
||||
elseif vim.startswith(line, "#") or line:find("^%s*$") then
|
||||
-- comment and blank line
|
||||
i = i + 1
|
||||
elseif vim.startswith(line, "priority") then
|
||||
snipmate_opts.priority = tonumber(line:match("priority%s+(%d+)"))
|
||||
i = i + 1
|
||||
else
|
||||
log.error("Invalid line in %s: %s", filename, i)
|
||||
error(("Invalid line in %s: %s"):format(filename, i))
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
snippets = snippets.snippet,
|
||||
autosnippets = snippets.autosnippet,
|
||||
misc = extends,
|
||||
}
|
||||
end
|
||||
|
||||
-- cache snippets without filetype-association for reuse.
|
||||
Data.snipmate_cache = snippetcache.new(load_snipmate)
|
||||
|
||||
--- Collection watches all files that belong to a collection of snippets below
|
||||
--- some root, and registers new files.
|
||||
local Collection = {}
|
||||
local Collection_mt = {
|
||||
__index = Collection,
|
||||
}
|
||||
|
||||
local function snipmate_package_file_filter(fname)
|
||||
return fname:match("%.snippets$")
|
||||
end
|
||||
|
||||
function Collection.new(
|
||||
root,
|
||||
lazy,
|
||||
include_ft,
|
||||
exclude_ft,
|
||||
add_opts,
|
||||
lazy_watcher,
|
||||
fs_event_providers
|
||||
)
|
||||
local ft_filter = loader_util.ft_filter(include_ft, exclude_ft)
|
||||
local o = setmetatable({
|
||||
root = root,
|
||||
|
||||
--- @alias LuaSnip.Loaders.Snipmate.FileCategory
|
||||
--- | '"collection"' File only belongs to the collection
|
||||
--- | '"load"' File should be loaded
|
||||
|
||||
--- Determine whether a file should be loaded, belongs to the
|
||||
--- collection, or doesn't.
|
||||
--- This distinction is important because we need to know about all
|
||||
--- files to correctly resolve `extend <someft>`, but only want to load
|
||||
--- the filetypes allowed by in/exclude.
|
||||
--- @param path string
|
||||
---@return LuaSnip.Loaders.Snipmate.FileCategory?
|
||||
categorize_file = function(path)
|
||||
if not path:sub(1, #root) == root then
|
||||
log.warn(
|
||||
"Tried to filter file `%s`, which is not inside the root `%s`.",
|
||||
path,
|
||||
root
|
||||
)
|
||||
return nil
|
||||
end
|
||||
if snipmate_package_file_filter(path) then
|
||||
local path_ft = loader_util.collection_file_ft(root, path)
|
||||
if ft_filter(path_ft) then
|
||||
return "load"
|
||||
end
|
||||
return "collection"
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
-- sometimes we don't want the full categorize-file-data, only which
|
||||
-- filetypes should be loaded.
|
||||
ft_filter = ft_filter,
|
||||
|
||||
add_opts = add_opts,
|
||||
lazy = lazy,
|
||||
-- store ft -> set of files that should be lazy-loaded.
|
||||
lazy_files = autotable(2, { warn = false }),
|
||||
-- store for each path the set of filetypes it has been loaded with.
|
||||
loaded_path_fts = autotable(2, { warn = false }),
|
||||
-- model filetype-extensions (`extends <someft>` in `ft.snippets`).
|
||||
-- Better than a flat table with t[ft] = {someft=true, somotherft=true}
|
||||
-- since transitive dependencies are easier to understand/query.
|
||||
-- There is an edge with source src to destination dst, if snippets for
|
||||
-- filetype src also contribute to filetype dst.
|
||||
-- Since we respect transitive `extends`, we can get all filetypes a
|
||||
-- snippet-file for some filetype A contributes to by querying the
|
||||
-- connected component of A (all filetype-vertices reachable from A).
|
||||
ft_extensions = digraph.new_labeled(),
|
||||
-- store all files in the collection, by their filetype.
|
||||
-- This information is necessary to handle `extends` even for files
|
||||
-- that are not actually loaded (due to in/exclude).
|
||||
collection_files_by_ft = autotable(2, { warn = false }),
|
||||
-- set if creation successful.
|
||||
watcher = nil,
|
||||
}, Collection_mt)
|
||||
|
||||
-- only register files up to a depth of 2.
|
||||
local ok, err_or_watcher = pcall(tree_watcher, root, 2, {
|
||||
-- don't handle removals for now.
|
||||
new_file = function(path)
|
||||
---@as LuaSnip.Loaders.Snipmate.FileCategory
|
||||
local file_category = o.categorize_file(path)
|
||||
|
||||
if file_category then
|
||||
-- know it's at least in the collection -> can register it.
|
||||
local file_ft = loader_util.collection_file_ft(o.root, path)
|
||||
o:register_file(path, file_ft)
|
||||
if file_category == "load" then
|
||||
-- actually load if allowed by in/exclude.
|
||||
o:add_file(path, file_ft)
|
||||
end
|
||||
end
|
||||
end,
|
||||
change_file = function(path)
|
||||
vim.schedule_wrap(function()
|
||||
o:reload(path)
|
||||
end)()
|
||||
end,
|
||||
}, { lazy = lazy_watcher, fs_event_providers = fs_event_providers })
|
||||
|
||||
if not ok then
|
||||
error(("Could not create watcher: %s"):format(err_or_watcher))
|
||||
end
|
||||
|
||||
o.watcher = err_or_watcher
|
||||
|
||||
log.info("Initialized snippet-collection at `%s`", root)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
--- Makes the file known to the collection, but does not load its snippets.
|
||||
--- This is important because `extends` may require loading a file excluded by
|
||||
--- `file_filter`, ie `include` and `exclude`.
|
||||
--- @param path string
|
||||
--- @param ft string
|
||||
function Collection:register_file(path, ft)
|
||||
self.collection_files_by_ft[ft][path] = true
|
||||
end
|
||||
|
||||
--- Register a file-filetype-association with the collection.
|
||||
--- @param path string Path to a file that belongs to this collection.
|
||||
--- @param add_ft string The original filetype this file should be added as.
|
||||
--- Since we have to support filetype-extensions, this may
|
||||
--- add the snippets of the file to several other
|
||||
--- filetypes.
|
||||
function Collection:add_file(path, add_ft)
|
||||
-- register known file.
|
||||
Data.snipmate_ft_paths[add_ft][path] = true
|
||||
|
||||
if self.lazy then
|
||||
if not session.loaded_fts[add_ft] then
|
||||
log.info(
|
||||
"Registering lazy-load-snippets for ft `%s` from file `%s`",
|
||||
add_ft,
|
||||
path
|
||||
)
|
||||
|
||||
-- only register to load later.
|
||||
self.lazy_files[add_ft][path] = true
|
||||
return
|
||||
else
|
||||
log.info(
|
||||
"Filetype `%s` is already active, loading immediately.",
|
||||
add_ft
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- extended filetypes will be loaded in load_file.
|
||||
self:load_file(path, add_ft, "SkipIfLoaded")
|
||||
end
|
||||
|
||||
--- @alias LuaSnip.Loaders.Snipmate.SkipLoad
|
||||
--- | '"ForceLoad"' Always load, even if it was already loaded.
|
||||
--- | '"SkipIfLoaded"' Skip the load if the file has been loaded already.
|
||||
|
||||
-- loads the fts that extend load_ft as well.
|
||||
-- skip_load_mode allows this code to both prevent unnecessary loads (which
|
||||
-- could be caused if some file is added to the same filetype more than once),
|
||||
-- while still handling reload (where the files has to be loaded again for
|
||||
-- every filetype, even if it already is loaded (since it may have different
|
||||
-- snippets))
|
||||
function Collection:load_file(path, ft, skip_load_mode)
|
||||
if skip_load_mode == "SkipIfLoaded" and self.loaded_path_fts[path][ft] then
|
||||
return
|
||||
end
|
||||
-- ignore load_file if ft is excluded/not included.
|
||||
if not self.ft_filter(ft) then
|
||||
return
|
||||
end
|
||||
|
||||
log.info("Adding snippets for filetype `%s` from file `%s`", ft, path)
|
||||
|
||||
-- Set here to skip loads triggered for the same path-file-combination in
|
||||
-- subsequent code, which would trigger and endless loop.
|
||||
self.loaded_path_fts[path][ft] = true
|
||||
|
||||
-- this may already be set, but setting again here ensures that a file is
|
||||
-- certainly associated with each filetype it's loaded for. (for example,
|
||||
-- file-ft-combinations loaded as a dependency from another file may not be
|
||||
-- set already).
|
||||
Data.snipmate_ft_paths[ft][path] = true
|
||||
|
||||
-- snippets may already be loaded -> get them from cache.
|
||||
local data = Data.snipmate_cache:fetch(path)
|
||||
local snippets = data.snippets
|
||||
local autosnippets = data.autosnippets
|
||||
-- data.misc is user-input, clean it here.
|
||||
local extended_fts = util.deduplicate(data.misc)
|
||||
|
||||
-- ignored if it already exists.
|
||||
self.ft_extensions:set_vertex(ft)
|
||||
-- make sure we don't retain any old dependencies.
|
||||
self.ft_extensions:clear_edges(path)
|
||||
|
||||
for _, extended_ft in pairs(extended_fts) do
|
||||
-- ignored if it already exists.
|
||||
self.ft_extensions:set_vertex(extended_ft)
|
||||
-- snippets for extended_ft should also be loaded if ft is loaded
|
||||
-- label edge with path, so all edges from this file can be updated on
|
||||
-- reload.
|
||||
self.ft_extensions:set_edge(extended_ft, ft, path)
|
||||
end
|
||||
|
||||
loader_util.add_file_snippets(
|
||||
ft,
|
||||
path,
|
||||
snippets,
|
||||
autosnippets,
|
||||
self.add_opts
|
||||
)
|
||||
|
||||
-- get all filetypes this one extends (directly or transitively), and load
|
||||
-- their files for ft.
|
||||
local load_fts = self.ft_extensions:connected_component(ft, "Backward")
|
||||
for _, extended_ft in ipairs(load_fts) do
|
||||
for file, _ in pairs(self.collection_files_by_ft[extended_ft]) do
|
||||
-- check which filetypes a file with filetype extended_ft has to be
|
||||
-- loaded for currently!! (ie. which filetypes directly or
|
||||
-- transitively extend extended_ft.
|
||||
-- The reason we can't just check that statically once, or just load
|
||||
-- a file for the filetypes its filetype is initially extended by is
|
||||
-- that the extends-graph may change on each `load_file`, and we
|
||||
-- want to respect these changes.
|
||||
for _, file_ft in
|
||||
ipairs(
|
||||
self.ft_extensions:connected_component(
|
||||
extended_ft,
|
||||
"Forward"
|
||||
)
|
||||
)
|
||||
do
|
||||
-- skips load if the file is already loaded for the given filetype.
|
||||
-- One bad side-effect of this current implementation is that
|
||||
-- the edges in the graph will be reset/set multiple times,
|
||||
-- until they are retained in the last load_file-call to the
|
||||
-- last filetype.
|
||||
self:load_file(file, file_ft, "SkipIfLoaded")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
refresh_notify(ft)
|
||||
end
|
||||
|
||||
function Collection:do_lazy_load(lazy_ft)
|
||||
for file, _ in pairs(self.lazy_files[lazy_ft]) do
|
||||
for _, ft in
|
||||
ipairs(self.ft_extensions:connected_component(lazy_ft, "Forward"))
|
||||
do
|
||||
-- skips load if the file is already loaded for the given filetype.
|
||||
self:load_file(file, ft, "SkipIfLoaded")
|
||||
end
|
||||
end
|
||||
end
|
||||
-- will only do something, if the file at `path` is actually in the collection.
|
||||
function Collection:reload(path)
|
||||
local loaded_fts = tbl_util.set_to_list(self.loaded_path_fts[path])
|
||||
for _, loaded_ft in ipairs(loaded_fts) do
|
||||
-- will override previously-loaded snippets from this path.
|
||||
self:load_file(path, loaded_ft, "ForceLoad")
|
||||
end
|
||||
|
||||
-- clean snippets if enough were removed.
|
||||
clean_invalidated()
|
||||
end
|
||||
|
||||
function Collection:stop()
|
||||
self.watcher:stop()
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
function M._load_lazy_loaded_ft(ft)
|
||||
log.info("Loading lazy-load-snippets for filetype `%s`", ft)
|
||||
|
||||
for _, collection in ipairs(Data.snipmate_collections) do
|
||||
collection:do_lazy_load(ft)
|
||||
end
|
||||
end
|
||||
|
||||
--- Generalized loading of collections.
|
||||
--- @param lazy boolean Whether the collection should be loaded lazily.
|
||||
--- @param opts LuaSnip.Loaders.LoadOpts?
|
||||
local function _load(lazy, opts)
|
||||
local o = loader_util.normalize_opts(opts)
|
||||
|
||||
local collection_roots = loader_util.resolve_root_paths(o.paths, "snippets")
|
||||
local lazy_roots = loader_util.resolve_lazy_root_paths(o.lazy_paths)
|
||||
|
||||
log.info(
|
||||
"Found roots `%s` for paths `%s`.",
|
||||
vim.inspect(collection_roots),
|
||||
vim.inspect(o.paths)
|
||||
)
|
||||
if o.paths and #o.paths ~= #collection_roots then
|
||||
log.warn(
|
||||
"Could not resolve all collection-roots for paths `%s`: only found `%s`",
|
||||
vim.inspect(o.paths),
|
||||
vim.inspect(collection_roots)
|
||||
)
|
||||
end
|
||||
|
||||
log.info(
|
||||
"Determined roots `%s` for lazy_paths `%s`.",
|
||||
vim.inspect(lazy_roots),
|
||||
vim.inspect(o.lazy_paths)
|
||||
)
|
||||
if o.lazy_paths and #o.lazy_paths ~= #lazy_roots then
|
||||
log.warn(
|
||||
"Could not resolve all collection-roots for lazy_paths `%s`: only found `%s`",
|
||||
vim.inspect(o.lazy_paths),
|
||||
vim.inspect(lazy_roots)
|
||||
)
|
||||
end
|
||||
|
||||
for paths_lazy, roots in pairs({
|
||||
[true] = lazy_roots,
|
||||
[false] = collection_roots,
|
||||
}) do
|
||||
for _, collection_root in ipairs(roots) do
|
||||
local ok, coll_or_err = pcall(
|
||||
Collection.new,
|
||||
collection_root,
|
||||
lazy,
|
||||
o.include,
|
||||
o.exclude,
|
||||
o.add_opts,
|
||||
paths_lazy,
|
||||
o.fs_event_providers
|
||||
)
|
||||
|
||||
if not ok then
|
||||
log.error(
|
||||
"Could not create collection at %s: %s",
|
||||
collection_root,
|
||||
coll_or_err
|
||||
)
|
||||
else
|
||||
table.insert(Data.snipmate_collections, coll_or_err)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Load snipmate-snippet-collections immediately.
|
||||
--- @param opts LuaSnip.Loaders.LoadOpts?
|
||||
function M.load(opts)
|
||||
_load(false, opts)
|
||||
end
|
||||
|
||||
--- Load snipmate-snippet-collections on demand.
|
||||
--- @param opts LuaSnip.Loaders.LoadOpts?
|
||||
function M.lazy_load(opts)
|
||||
_load(true, opts)
|
||||
-- load for current buffer on startup.
|
||||
for _, ft in
|
||||
ipairs(loader_util.get_load_fts(vim.api.nvim_get_current_buf()))
|
||||
do
|
||||
M._load_lazy_loaded_ft(ft)
|
||||
end
|
||||
end
|
||||
|
||||
function M.clean()
|
||||
for _, collection in ipairs(Data.snipmate_collections) do
|
||||
collection:stop()
|
||||
end
|
||||
Data.snipmate_ft_paths = autotable(2)
|
||||
-- don't reset cache, snippets are correctly updated on file-change anyway,
|
||||
-- and there is no persistent data passed on.
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,645 @@
|
||||
local util = require("luasnip.util.util")
|
||||
local loader_util = require("luasnip.loaders.util")
|
||||
local Path = require("luasnip.util.path")
|
||||
local log = require("luasnip.util.log").new("vscode-loader")
|
||||
local autotable = require("luasnip.util.auto_table").autotable
|
||||
local path_watcher = require("luasnip.loaders.fs_watchers").path
|
||||
local Data = require("luasnip.loaders.data")
|
||||
local session = require("luasnip.session")
|
||||
local refresh_notify =
|
||||
require("luasnip.session.enqueueable_operations").refresh_notify
|
||||
local clean_invalidated =
|
||||
require("luasnip.session.enqueueable_operations").clean_invalidated
|
||||
|
||||
-- create snippetProxy which does not trim lines and dedent text.
|
||||
-- It's fair to use passed test as-is, if it's from json.
|
||||
local parse = require("luasnip.util.parser").parse_snippet
|
||||
local sp = require("luasnip.util.extend_decorator").apply(
|
||||
require("luasnip.nodes.snippetProxy"),
|
||||
{},
|
||||
{
|
||||
parse_fn = function(ctx, body)
|
||||
return parse(ctx, body, { trim_empty = false, dedent = false })
|
||||
end,
|
||||
}
|
||||
)
|
||||
|
||||
local json_decoders = {
|
||||
json = util.json_decode,
|
||||
jsonc = require("luasnip.util.jsonc").decode,
|
||||
["code-snippets"] = require("luasnip.util.jsonc").decode,
|
||||
}
|
||||
|
||||
local function read_json(fname)
|
||||
local data_ok, data = pcall(Path.read_file, fname)
|
||||
if not data_ok then
|
||||
log.error("Could not read file %s", fname)
|
||||
return nil
|
||||
end
|
||||
|
||||
local fname_extension = Path.extension(fname)
|
||||
if json_decoders[fname_extension] == nil then
|
||||
log.error(
|
||||
"`%s` was expected to have file-extension either `json`, `jsonc` or `code-snippets`, but doesn't.",
|
||||
fname
|
||||
)
|
||||
return nil
|
||||
end
|
||||
local fname_decoder = json_decoders[fname_extension]
|
||||
|
||||
local status, result = pcall(fname_decoder, data)
|
||||
if status then
|
||||
return result
|
||||
else
|
||||
log.error("Could not parse file %s: %s", fname, result)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Load snippets from vscode-snippet-file.
|
||||
--- @param file string Path to file
|
||||
---@return LuaSnip.Loaders.SnippetFileData
|
||||
local function get_file_snippets(file)
|
||||
local source = require("luasnip.session.snippet_collection.source")
|
||||
local multisnippet = require("luasnip.nodes.multiSnippet")
|
||||
|
||||
-- since most snippets we load don't have a scope-field, we just insert
|
||||
-- them here by default.
|
||||
local snippets = {}
|
||||
|
||||
local snippet_set_data = read_json(file)
|
||||
if snippet_set_data == nil then
|
||||
log.error("Reading json from file `%s` failed, skipping it.", file)
|
||||
|
||||
return {
|
||||
snippets = {},
|
||||
autosnippets = {},
|
||||
misc = {},
|
||||
}
|
||||
end
|
||||
|
||||
for name, parts in pairs(snippet_set_data) do
|
||||
local body = type(parts.body) == "string" and parts.body
|
||||
or table.concat(parts.body, "\n")
|
||||
|
||||
local ls_conf = parts.luasnip or {}
|
||||
|
||||
-- we may generate multiple interfaces to the same snippet
|
||||
-- (different filetype, different triggers)
|
||||
|
||||
-- context common to all snippets generated here.
|
||||
local common_context = {
|
||||
name = name,
|
||||
desc = parts.description or name,
|
||||
wordTrig = ls_conf.wordTrig,
|
||||
priority = ls_conf.priority,
|
||||
snippetType = ls_conf.autotrigger and "autosnippet" or "snippet",
|
||||
}
|
||||
|
||||
-- Sometimes it's a list of prefixes instead of a single one
|
||||
local prefixes = type(parts.prefix) == "table" and parts.prefix
|
||||
or { parts.prefix }
|
||||
|
||||
-- vscode documents `,`, but `.` also works.
|
||||
-- an entry `false` in this list will cause a `ft=nil` for the snippet.
|
||||
local filetypes = parts.scope and vim.split(parts.scope, "[.,]")
|
||||
or { false }
|
||||
|
||||
local contexts = {}
|
||||
for _, prefix in ipairs(prefixes) do
|
||||
for _, filetype in ipairs(filetypes) do
|
||||
table.insert(
|
||||
contexts,
|
||||
{ filetype = filetype or nil, trig = prefix }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local snip
|
||||
if #contexts > 1 then
|
||||
-- only construct multisnippet if it is actually necessary.
|
||||
contexts.common = common_context
|
||||
snip = multisnippet._raw_ms(contexts, sp(nil, body), {})
|
||||
elseif #contexts == 1 then
|
||||
-- have to add options from common context to the trig/filetype-context.
|
||||
snip = sp(vim.tbl_extend("keep", contexts[1], common_context), body)
|
||||
end
|
||||
|
||||
if snip then
|
||||
if session.config.loaders_store_source then
|
||||
-- only know file, not line or line_end.
|
||||
snip._source = source.from_location(file)
|
||||
end
|
||||
|
||||
table.insert(snippets, snip)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
snippets = snippets,
|
||||
autosnippets = {},
|
||||
misc = {},
|
||||
}
|
||||
end
|
||||
|
||||
-- has to be set in separate module to allow different module-path-separators
|
||||
-- in `require`.
|
||||
Data.vscode_cache =
|
||||
require("luasnip.loaders.snippet_cache").new(get_file_snippets)
|
||||
|
||||
--- Parse package.json(c), determine all files that contribute snippets, and
|
||||
--- which filetype is associated with them.
|
||||
--- @param manifest string
|
||||
--- @return table<string, table<string, true|nil>>
|
||||
local function get_snippet_files(manifest)
|
||||
-- if root doesn't contain a package.json, or it contributes no snippets,
|
||||
-- return no snippets.
|
||||
if not Path.exists(manifest) then
|
||||
log.warn("Manifest %s does not exist", manifest)
|
||||
return {}
|
||||
end
|
||||
|
||||
local package_data = read_json(manifest)
|
||||
if not package_data then
|
||||
-- since it is a `.json/jsonc`, the json not being correct should be an error.
|
||||
log.error("Could not read json from `%s`", manifest)
|
||||
return {}
|
||||
end
|
||||
|
||||
if
|
||||
not package_data.contributes or not package_data.contributes.snippets
|
||||
then
|
||||
log.warn("Manifest %s does not contribute any snippets.", manifest)
|
||||
return {}
|
||||
end
|
||||
|
||||
-- stores ft -> files -> true|nil, allow iterating files and their
|
||||
-- filetypes while preventing duplicates.
|
||||
local ft_file_set = autotable(2, { warn = false })
|
||||
|
||||
-- parent-directory of package.json(c), all files in the package.json(c)
|
||||
-- are relative to it.
|
||||
local package_parent = Path.parent(manifest)
|
||||
|
||||
for _, snippet_entry in pairs(package_data.contributes.snippets) do
|
||||
local absolute_path = Path.join(package_parent, snippet_entry.path)
|
||||
|
||||
local normalized_snippet_file = Path.normalize(absolute_path)
|
||||
|
||||
if not normalized_snippet_file then
|
||||
-- path does not exist (yet), try and guess the correct path anyway.
|
||||
normalized_snippet_file = Path.normalize_nonexisting(absolute_path)
|
||||
log.warn(
|
||||
"Could not find file %s advertised in %s, guessing %s as the absolute and normalized path.",
|
||||
absolute_path,
|
||||
manifest,
|
||||
normalized_snippet_file
|
||||
)
|
||||
end
|
||||
|
||||
local langs = snippet_entry.language
|
||||
if type(langs) ~= "table" then
|
||||
langs = { langs }
|
||||
end
|
||||
for _, ft in ipairs(langs) do
|
||||
ft_file_set[ft][normalized_snippet_file] = true
|
||||
end
|
||||
end
|
||||
|
||||
return ft_file_set
|
||||
end
|
||||
|
||||
-- Responsible for watching a single json-snippet-file.
|
||||
local SnippetfileWatcher = {}
|
||||
local SnippetfileWatcher_mt = { __index = SnippetfileWatcher }
|
||||
|
||||
function SnippetfileWatcher.new(
|
||||
path,
|
||||
initial_ft,
|
||||
fs_event_providers,
|
||||
lazy,
|
||||
load_cb
|
||||
)
|
||||
local o = setmetatable({
|
||||
path = path,
|
||||
load_cb = load_cb,
|
||||
-- track which filetypes this file has been loaded for, so we can
|
||||
-- reload for all of them.
|
||||
loaded_fts = { [initial_ft] = true },
|
||||
}, SnippetfileWatcher_mt)
|
||||
|
||||
local load_all_fts = function()
|
||||
for ft, _ in pairs(o.loaded_fts) do
|
||||
load_cb(path, ft)
|
||||
refresh_notify(ft)
|
||||
end
|
||||
end
|
||||
local ok, err_or_watcher = pcall(path_watcher, path, {
|
||||
add = load_all_fts,
|
||||
change = function()
|
||||
load_all_fts()
|
||||
|
||||
-- clean snippets if enough were removed.
|
||||
clean_invalidated()
|
||||
end,
|
||||
}, { lazy = lazy, fs_event_providers = fs_event_providers })
|
||||
|
||||
if not ok then
|
||||
-- has to be handled by caller, we can't really proceed if the creation
|
||||
-- failed.
|
||||
error(
|
||||
("Could not create path_watcher for path %s: %s"):format(
|
||||
path,
|
||||
err_or_watcher
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
o.watcher = err_or_watcher
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
-- called by collection.
|
||||
function SnippetfileWatcher:add_ft(ft)
|
||||
if self.loaded_fts[ft] then
|
||||
-- already loaded.
|
||||
return
|
||||
end
|
||||
self.loaded_fts[ft] = true
|
||||
self.load_cb(self.path, ft)
|
||||
end
|
||||
|
||||
function SnippetfileWatcher:stop()
|
||||
self.watcher:stop()
|
||||
end
|
||||
|
||||
--- Collection watches all files that belong to a collection of snippets below
|
||||
--- some root, and registers new files.
|
||||
local Collection = {}
|
||||
local Collection_mt = {
|
||||
__index = Collection,
|
||||
}
|
||||
|
||||
function Collection.new(
|
||||
manifest_path,
|
||||
lazy,
|
||||
include_ft,
|
||||
exclude_ft,
|
||||
add_opts,
|
||||
lazy_watcher,
|
||||
fs_event_providers
|
||||
)
|
||||
local ft_filter = loader_util.ft_filter(include_ft, exclude_ft)
|
||||
local o = setmetatable({
|
||||
lazy = lazy,
|
||||
-- store ft -> set of files that should be lazy-loaded.
|
||||
lazy_files = autotable(2, { warn = false }),
|
||||
fs_event_providers = fs_event_providers,
|
||||
|
||||
-- store path-watchers (so we don't register more than one for one
|
||||
-- path), and so we can disable them.
|
||||
path_watchers = {},
|
||||
-- for really loading a file.
|
||||
-- this is not done in Collection:load itself, since it may have to be
|
||||
-- performed as a callback on file-creation.
|
||||
load_callback = function(path, ft)
|
||||
local data = Data.vscode_cache:fetch(path)
|
||||
-- autosnippets are included in snippets for this loader.
|
||||
local snippets = data.snippets
|
||||
loader_util.add_file_snippets(ft, path, snippets, {}, add_opts)
|
||||
end,
|
||||
-- initialized in a bit, we have to store+reset a watcher for the manifest-file.
|
||||
manifest_watcher = nil,
|
||||
}, Collection_mt)
|
||||
|
||||
-- callback for updating the file-filetype-associations from the manifest.
|
||||
local update_manifest = function()
|
||||
local manifest_ft_paths = get_snippet_files(manifest_path)
|
||||
for ft, path_set in pairs(manifest_ft_paths) do
|
||||
if ft_filter(ft) then
|
||||
for path, _ in pairs(path_set) do
|
||||
o:add_file(path, ft)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ok, watcher_or_err = pcall(path_watcher, manifest_path, {
|
||||
-- don't handle removals for now.
|
||||
add = update_manifest,
|
||||
change = update_manifest,
|
||||
}, { lazy = lazy_watcher, fs_event_providers = fs_event_providers })
|
||||
|
||||
if not ok then
|
||||
error(("Could not create watcher: %s"):format(watcher_or_err))
|
||||
end
|
||||
o.manifest_watcher = watcher_or_err
|
||||
|
||||
log.info("Initialized snippet-collection with manifest %s", manifest_path)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
-- Add file with some filetype to collection, load according to lazy_load.
|
||||
function Collection:add_file(path, ft)
|
||||
Data.vscode_ft_paths[ft][path] = true
|
||||
|
||||
if self.lazy then
|
||||
if not session.loaded_fts[ft] then
|
||||
log.info(
|
||||
"Registering lazy-load-snippets for ft `%s` from file `%s`",
|
||||
ft,
|
||||
path
|
||||
)
|
||||
|
||||
-- only register to load later.
|
||||
self.lazy_files[ft][path] = true
|
||||
return
|
||||
else
|
||||
log.info(
|
||||
"Filetype `%s` is already active, loading immediately.",
|
||||
ft
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
self:load_file(path, ft)
|
||||
end
|
||||
|
||||
function Collection:load_file(path, ft)
|
||||
log.info("Registering file %s with filetype %s for loading.", path, ft)
|
||||
if not self.path_watchers[path] then
|
||||
-- always register these lazily, that way an upate to the package.json
|
||||
-- without the snippet-file existing will work!
|
||||
-- Also make sure we use the same fs_event_providers.
|
||||
local ok, watcher_or_err = pcall(
|
||||
SnippetfileWatcher.new,
|
||||
path,
|
||||
ft,
|
||||
self.fs_event_providers,
|
||||
true,
|
||||
self.load_callback
|
||||
)
|
||||
if not ok then
|
||||
log.error(
|
||||
"Could not create SnippetFileWatcher for path %s: %s",
|
||||
path,
|
||||
watcher_or_err
|
||||
)
|
||||
return
|
||||
end
|
||||
self.path_watchers[path] = watcher_or_err
|
||||
else
|
||||
-- make new filetype known to existing watcher.
|
||||
self.path_watchers[path]:add_ft(ft)
|
||||
end
|
||||
end
|
||||
|
||||
-- stop all watchers associated with this collection, to make sure no snippets
|
||||
-- are added from this collection again.
|
||||
function Collection:stop()
|
||||
self.manifest_watcher:stop()
|
||||
for _, watcher in pairs(self.path_watchers) do
|
||||
watcher:stop()
|
||||
end
|
||||
end
|
||||
|
||||
function Collection:do_lazy_load(ft)
|
||||
for file, _ in pairs(self.lazy_files[ft]) do
|
||||
self:load_file(file, ft)
|
||||
end
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
local function get_rtp_paths()
|
||||
return vim.list_extend(
|
||||
-- would be very surprised if this yields duplicates :D
|
||||
vim.api.nvim_get_runtime_file("package.json", true),
|
||||
vim.api.nvim_get_runtime_file("package.jsonc", true)
|
||||
)
|
||||
end
|
||||
|
||||
--- Generate list of manifest-paths from list of directory-paths.
|
||||
--- If nil, search rtp.
|
||||
--- If a given directory, or the mani
|
||||
---
|
||||
--- @param paths string|table? List of existing directories. If nil, search runtimepath.
|
||||
---@return string[] manifest_paths
|
||||
local function get_manifests(paths)
|
||||
local manifest_paths = {}
|
||||
-- list of paths to crawl for loading (could be a table or a comma-separated-list)
|
||||
if paths then
|
||||
-- Get path to package.json/package.jsonc, or continue if it does not exist.
|
||||
for _, dir in ipairs(paths) do
|
||||
local tentative_manifest_path =
|
||||
Path.expand_keep_symlink(Path.join(dir, "package.json"))
|
||||
if Path.exists(tentative_manifest_path) then
|
||||
table.insert(manifest_paths, tentative_manifest_path)
|
||||
else
|
||||
tentative_manifest_path =
|
||||
Path.expand_keep_symlink(Path.join(dir, "package.jsonc"))
|
||||
if Path.exists(tentative_manifest_path) then
|
||||
table.insert(manifest_paths, tentative_manifest_path)
|
||||
else
|
||||
log.warn(
|
||||
"Could not find package.json(c) in path %s (expanded to %s).",
|
||||
dir,
|
||||
Path.expand(dir)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
manifest_paths = get_rtp_paths()
|
||||
end
|
||||
|
||||
return manifest_paths
|
||||
end
|
||||
|
||||
--- Generate list of paths to manifests that may not yet exist, from list of
|
||||
--- directories (which also may not yet exist).
|
||||
--- One peculiarity: This will generate two paths for each directory, since we
|
||||
--- don't know if the package.json or the package.jsonc will be created.
|
||||
--- This may cause a bit of overhead (not much due to snippet-cache) if both
|
||||
--- are created and contribute the same snippets, but that's unlikely and/or
|
||||
--- user error :P
|
||||
--- @param paths string[]
|
||||
---@return string[]
|
||||
local function get_lazy_manifests(paths)
|
||||
local lazy_manifest_paths = {}
|
||||
if paths then
|
||||
-- list of directories, convert to list of existing manifest-files.
|
||||
if type(paths) == "string" then
|
||||
paths = vim.split(paths, ",")
|
||||
end
|
||||
for _, dir in ipairs(paths) do
|
||||
local absolute_dir = Path.expand_maybe_nonexisting(dir)
|
||||
|
||||
table.insert(
|
||||
lazy_manifest_paths,
|
||||
Path.join(absolute_dir, "package.json")
|
||||
)
|
||||
table.insert(
|
||||
lazy_manifest_paths,
|
||||
Path.join(absolute_dir, "package.jsonc")
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return lazy_manifest_paths
|
||||
end
|
||||
|
||||
local function _load(lazy, opts)
|
||||
local o = loader_util.normalize_opts(opts)
|
||||
|
||||
local manifests = get_manifests(o.paths)
|
||||
local lazy_manifests = get_lazy_manifests(o.lazy_paths)
|
||||
|
||||
log.info(
|
||||
"Found manifests `%s` for paths `%s`.",
|
||||
vim.inspect(manifests),
|
||||
vim.inspect(o.paths)
|
||||
)
|
||||
if o.paths and #o.paths ~= #manifests then
|
||||
log.warn(
|
||||
"Could not resolve all manifests for paths `%s`: only found `%s`",
|
||||
vim.inspect(o.paths),
|
||||
vim.inspect(manifests)
|
||||
)
|
||||
end
|
||||
|
||||
log.info(
|
||||
"Determined roots `%s` for lazy_paths `%s`.",
|
||||
vim.inspect(lazy_manifests),
|
||||
vim.inspect(o.lazy_paths)
|
||||
)
|
||||
-- two lazy manifests from each lazy directory.
|
||||
if o.lazy_paths and #o.lazy_paths ~= 2 * #lazy_manifests then
|
||||
log.warn(
|
||||
"Could not resolve all manifests for lazy_paths `%s`: only found `%s`",
|
||||
vim.inspect(o.lazy_paths),
|
||||
vim.inspect(lazy_manifests)
|
||||
)
|
||||
end
|
||||
|
||||
for is_lazy, manifest_paths in pairs({
|
||||
[true] = lazy_manifests,
|
||||
[false] = manifests,
|
||||
}) do
|
||||
for _, manifest_path in ipairs(manifest_paths) do
|
||||
local ok, coll_or_err = pcall(
|
||||
Collection.new,
|
||||
manifest_path,
|
||||
lazy,
|
||||
o.include,
|
||||
o.exclude,
|
||||
o.add_opts,
|
||||
is_lazy,
|
||||
o.fs_event_providers
|
||||
)
|
||||
|
||||
if not ok then
|
||||
log.error(
|
||||
"Could not create collection for manifest %s: %s",
|
||||
manifest_path,
|
||||
coll_or_err
|
||||
)
|
||||
else
|
||||
table.insert(Data.vscode_package_collections, coll_or_err)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M._load_lazy_loaded_ft(ft)
|
||||
log.info("Loading lazy-load-snippets for filetype `%s`", ft)
|
||||
|
||||
for _, collection in ipairs(Data.vscode_package_collections) do
|
||||
collection:do_lazy_load(ft)
|
||||
end
|
||||
-- no need to lazy_load standalone-snippets.
|
||||
end
|
||||
|
||||
function M.load(opts)
|
||||
_load(false, opts)
|
||||
end
|
||||
|
||||
function M.lazy_load(opts)
|
||||
_load(true, opts)
|
||||
|
||||
-- load for current buffer on startup.
|
||||
for _, ft in
|
||||
ipairs(loader_util.get_load_fts(vim.api.nvim_get_current_buf()))
|
||||
do
|
||||
M._load_lazy_loaded_ft(ft)
|
||||
end
|
||||
end
|
||||
|
||||
function M.load_standalone(opts)
|
||||
opts = opts or {}
|
||||
|
||||
local lazy = vim.F.if_nil(opts.lazy, false)
|
||||
local add_opts = loader_util.make_add_opts(opts)
|
||||
local fs_event_providers =
|
||||
vim.F.if_nil(opts.fs_event_providers, { autocmd = true, libuv = false })
|
||||
|
||||
local path
|
||||
if not lazy then
|
||||
path = Path.expand(opts.path)
|
||||
if not path then
|
||||
log.error(
|
||||
"Expanding path %s does not produce an existing path.",
|
||||
opts.path
|
||||
)
|
||||
return
|
||||
end
|
||||
else
|
||||
path = Path.expand_maybe_nonexisting(opts.path)
|
||||
end
|
||||
|
||||
Data.vscode_ft_paths["all"][path] = true
|
||||
|
||||
local ok, watcher_or_err = pcall(
|
||||
SnippetfileWatcher.new,
|
||||
path,
|
||||
"all",
|
||||
fs_event_providers,
|
||||
lazy,
|
||||
function()
|
||||
local data = Data.vscode_cache:fetch(path)
|
||||
-- autosnippets are included in snippets for this loader.
|
||||
local snippets = data.snippets
|
||||
loader_util.add_file_snippets("all", path, snippets, {}, add_opts)
|
||||
end
|
||||
)
|
||||
|
||||
if not ok then
|
||||
log.error(
|
||||
"Could not create SnippetFileWatcher for path %s: %s",
|
||||
path,
|
||||
watcher_or_err
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
table.insert(Data.vscode_standalone_watchers, watcher_or_err)
|
||||
end
|
||||
|
||||
function M.clean()
|
||||
for _, collection in ipairs(Data.vscode_package_collections) do
|
||||
collection:stop()
|
||||
end
|
||||
Data.vscode_package_collections = {}
|
||||
for _, standalone_watcher in ipairs(Data.vscode_standalone_watchers) do
|
||||
standalone_watcher:stop()
|
||||
end
|
||||
Data.vscode_standalone_watchers = {}
|
||||
|
||||
Data.vscode_ft_paths = autotable(2)
|
||||
-- don't reset cache, there's no reason to discard the already-loaded
|
||||
-- snippets as long as they're unchanged.
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,723 @@
|
||||
local Path = require("luasnip.util.path")
|
||||
local uv = vim.uv or vim.loop
|
||||
local util = require("luasnip.util.util")
|
||||
local log_tree = require("luasnip.util.log").new("tree-watcher")
|
||||
local log_path = require("luasnip.util.log").new("path-watcher")
|
||||
local log = require("luasnip.util.log").new("fs-watchers")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- used by both watchers.
|
||||
local callback_mt = {
|
||||
__index = function()
|
||||
return util.nop
|
||||
end,
|
||||
}
|
||||
|
||||
--- @alias LuaSnip.FSWatcher.FSEventProviders
|
||||
--- | '"autocmd"' Hook into BufWritePost to receive notifications on file-changes.
|
||||
--- | '"libuv"' Register uv.fs_event to receive notifications on file-changes.
|
||||
|
||||
--- @alias LuaSnip.FSWatcher.Callback fun(full_path: string)
|
||||
|
||||
--- @class LuaSnip.FSWatcher.TreeCallbacks
|
||||
--- @field new_file LuaSnip.FSWatcher.Callback?
|
||||
--- @field new_dir LuaSnip.FSWatcher.Callback?
|
||||
--- @field remove_file LuaSnip.FSWatcher.Callback?
|
||||
--- @field remove_dir LuaSnip.FSWatcher.Callback?
|
||||
--- @field remove_root LuaSnip.FSWatcher.Callback?
|
||||
--- @field change_file LuaSnip.FSWatcher.Callback?
|
||||
--- @field change_dir LuaSnip.FSWatcher.Callback?
|
||||
--- The callbacks are called with the full path to the file/directory that is
|
||||
--- affected.
|
||||
--- Callbacks that are not set will be replaced by a nop.
|
||||
|
||||
--- @class LuaSnip.FSWatcher.PathCallbacks
|
||||
--- @field add LuaSnip.FSWatcher.Callback?
|
||||
--- @field remove LuaSnip.FSWatcher.Callback?
|
||||
--- @field change LuaSnip.FSWatcher.Callback?
|
||||
--- The callbacks are called with the full path to the file that path-watcher
|
||||
--- is registered on.
|
||||
--- Callbacks that are not set will be replaced by a nop.
|
||||
|
||||
--- @class LuaSnip.FSWatcher.Options
|
||||
--- @field lazy boolean?
|
||||
--- If set, the watcher will be initialized even if the root/watched path does
|
||||
--- not yet exist, and start notifications once it is created.
|
||||
--- @field fs_event_providers table<LuaSnip.FSWatcher.FSEventProviders, boolean>?
|
||||
--- Which providers to use for receiving file-changes.
|
||||
|
||||
local function get_opts(opts)
|
||||
opts = opts or {}
|
||||
local lazy = vim.F.if_nil(opts.lazy, false)
|
||||
local fs_event_providers =
|
||||
vim.F.if_nil(opts.fs_event_providers, { autocmd = true, libuv = false })
|
||||
|
||||
return lazy, fs_event_providers
|
||||
end
|
||||
|
||||
-- plain list, don't use map-style table since we'll only need direct access to
|
||||
-- a watcher when it is stopped, which seldomly happens (at least, compared to
|
||||
-- how often it is iterated in the autocmd-callback).
|
||||
M.autocmd_watchers = {}
|
||||
|
||||
vim.api.nvim_create_augroup("_luasnip_fs_watcher", {})
|
||||
vim.api.nvim_create_autocmd({ "BufWritePost" }, {
|
||||
callback = function(args)
|
||||
log.debug("Received BufWritePost for file %s.", args.file)
|
||||
local realpath = Path.normalize(args.file)
|
||||
if not realpath then
|
||||
-- if nil, the path does not exist for some reason.
|
||||
log.info(
|
||||
"Registered BufWritePost with <afile> %s, but realpath does not exist. Aborting fs-watcher-notification.",
|
||||
args.file
|
||||
)
|
||||
return
|
||||
end
|
||||
log.debug(
|
||||
"Received update for file %s, using realpath %s.",
|
||||
args.file,
|
||||
realpath
|
||||
)
|
||||
|
||||
-- remove stopped watchers.
|
||||
-- Does not really matter whether we do this before or after the
|
||||
-- callbacks, since stopped watchers already take care to not do
|
||||
-- callbacks.
|
||||
-- Doing this during the callback-invocations, however, would incur
|
||||
-- some more complexity since ipairs does not support removal of
|
||||
-- elements during the iteration.
|
||||
M.autocmd_watchers = vim.tbl_filter(function(watcher)
|
||||
-- this won't catch unstarted watchers, since they can't be in this
|
||||
-- list in the first place.
|
||||
return not watcher.stopped
|
||||
end, M.autocmd_watchers)
|
||||
|
||||
for _, watcher in ipairs(M.autocmd_watchers) do
|
||||
watcher:BufWritePost_callback(realpath)
|
||||
end
|
||||
end,
|
||||
group = "_luasnip_fs_watcher",
|
||||
})
|
||||
|
||||
-- similar autocmd_watchers, only this list contains watchers that should be
|
||||
-- notified on a manual update (which right now is every watcher).
|
||||
M.active_watchers = {}
|
||||
function M.write_notify(realpath)
|
||||
M.active_watchers = vim.tbl_filter(function(watcher)
|
||||
-- this won't catch unstarted watchers, since they can't be in this
|
||||
-- list in the first place.
|
||||
return not watcher.stopped
|
||||
end, M.active_watchers)
|
||||
|
||||
for _, watcher in ipairs(M.active_watchers) do
|
||||
watcher:BufWritePost_callback(realpath)
|
||||
end
|
||||
end
|
||||
|
||||
--- @class LuaSnip.FSWatcher.Tree
|
||||
--- @field root string
|
||||
--- @field fs_event userdata
|
||||
--- @field files table<string, boolean>
|
||||
--- @field dir_watchers table<string, LuaSnip.FSWatcher.Tree>
|
||||
--- @field removed boolean
|
||||
--- @field stopped boolean
|
||||
--- @field callbacks LuaSnip.FSWatcher.TreeCallbacks
|
||||
--- @field depth number How deep the root should be monitored.
|
||||
--- @field fs_event_providers table<LuaSnip.FSWatcher.FSEventProviders, boolean>
|
||||
--- @field root_realpath string? Set as soon as the watcher is started.
|
||||
local TreeWatcher = {}
|
||||
local TreeWatcher_mt = {
|
||||
__index = TreeWatcher,
|
||||
}
|
||||
|
||||
function TreeWatcher:stop()
|
||||
for _, child_watcher in ipairs(self.dir_watchers) do
|
||||
child_watcher:stop()
|
||||
end
|
||||
self:stop_self()
|
||||
end
|
||||
|
||||
function TreeWatcher:stop_self()
|
||||
-- don't check which fs_event_providers were actually started, for both of
|
||||
-- these it should not matter if they weren't.
|
||||
self.stopped = true
|
||||
self.send_notifications = false
|
||||
|
||||
self.fs_event:stop()
|
||||
-- will be removed from active_watchers/autocmd_watchers after the next event, but already won't receive it.
|
||||
end
|
||||
|
||||
function TreeWatcher:fs_event_callback(err, relpath, events)
|
||||
if not self.send_notifications then
|
||||
-- abort if we should not send notifications anymore.
|
||||
return
|
||||
end
|
||||
vim.schedule_wrap(function()
|
||||
log_tree.debug(
|
||||
"raw: self.root: %s; err: %s; relpath: %s; change: %s; rename: %s",
|
||||
self.root,
|
||||
err,
|
||||
relpath,
|
||||
events.change,
|
||||
events.rename
|
||||
)
|
||||
local full_path = Path.join(self.root, relpath)
|
||||
local path_stat = uv.fs_stat(full_path)
|
||||
|
||||
-- try to figure out what happened in the directory.
|
||||
if events.rename then
|
||||
if not uv.fs_stat(self.root) then
|
||||
self:remove_root()
|
||||
return
|
||||
end
|
||||
if not path_stat then
|
||||
self:remove_child(relpath, full_path)
|
||||
return
|
||||
end
|
||||
|
||||
local f_type
|
||||
-- if there is a link to a directory, we are notified on changes!!
|
||||
if path_stat.type == "link" then
|
||||
f_type = uv.fs_stat(uv.fs_realpath(full_path))
|
||||
else
|
||||
f_type = path_stat.type
|
||||
end
|
||||
|
||||
if f_type == "file" then
|
||||
if self.files[relpath] then
|
||||
-- rename and file exists => a new file was moved into its
|
||||
-- place => handle as changed file.
|
||||
self:change_file(relpath, full_path)
|
||||
else
|
||||
self:new_file(relpath, full_path)
|
||||
end
|
||||
return
|
||||
elseif f_type == "directory" then
|
||||
if self.dir_watchers[relpath] then
|
||||
-- rename and directory exists => directory is overwritten
|
||||
-- => stop recursively, clear, and start a new watcher.
|
||||
self.dir_watchers[relpath]:stop()
|
||||
self.dir_watchers[relpath] = nil
|
||||
end
|
||||
self:new_dir(relpath, full_path)
|
||||
return
|
||||
end
|
||||
elseif events.change then
|
||||
self:change_child(relpath, full_path)
|
||||
end
|
||||
end)()
|
||||
end
|
||||
|
||||
-- May not recognize child correctly if there are symlinks on the path from the
|
||||
-- child to the directory-root.
|
||||
-- Should be fine, especially since, I think, fs_event can recognize those
|
||||
-- correctly, which means that this is an issue only very seldomly.
|
||||
function TreeWatcher:BufWritePost_callback(realpath)
|
||||
if not self.send_notifications then
|
||||
return
|
||||
end
|
||||
|
||||
if realpath:sub(1, #self.realpath_root) ~= self.realpath_root then
|
||||
-- not inside this root.
|
||||
return
|
||||
end
|
||||
|
||||
-- `#self.realpath_root+2`: remove root and path-separator.
|
||||
local root_relative_components =
|
||||
Path.components(realpath:sub(#self.realpath_root + 2))
|
||||
local rel = root_relative_components[1]
|
||||
if #root_relative_components == 1 then
|
||||
-- wrote file.
|
||||
-- either new, or changed.
|
||||
if self.files[rel] then
|
||||
-- use regular root for notifications!
|
||||
self:change_file(rel, Path.join(self.root, rel))
|
||||
else
|
||||
self:new_file(rel, Path.join(self.root, rel))
|
||||
end
|
||||
else
|
||||
if self.dir_watchers[rel] then
|
||||
if #root_relative_components == 2 then
|
||||
-- only notify if the changed file is immediately in the
|
||||
-- directory we're watching!
|
||||
-- I think this is the behaviour of fs_event, and logically
|
||||
-- makes sense.
|
||||
self:change_dir(rel, Path.join(self.root, rel))
|
||||
end
|
||||
else
|
||||
-- does nothing if the directory already exists.
|
||||
self:new_dir(rel, Path.join(self.root, rel))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TreeWatcher:start()
|
||||
if self.depth == 0 then
|
||||
-- don't watch children for 0-depth.
|
||||
return
|
||||
end
|
||||
|
||||
if self.stopped then
|
||||
-- stopping overrides and prevents starting.
|
||||
return
|
||||
end
|
||||
|
||||
self.send_notifications = true
|
||||
|
||||
if self.fs_event_providers.libuv then
|
||||
-- does not work on nfs-drive, at least if it's edited from another
|
||||
-- machine.
|
||||
local success, err = self.fs_event:start(
|
||||
self.root,
|
||||
{},
|
||||
function(err, relpath, events)
|
||||
self:fs_event_callback(err, relpath, events)
|
||||
end
|
||||
)
|
||||
|
||||
if not success then
|
||||
log_tree.error(
|
||||
"Could not start libuv-monitor for path %s due to error %s",
|
||||
self.path,
|
||||
err
|
||||
)
|
||||
else
|
||||
log_tree.info(
|
||||
"Monitoring root-directory %s with libuv-monitor.",
|
||||
self.root
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- needed by BufWritePost-callback.
|
||||
self.realpath_root = Path.normalize(self.root)
|
||||
|
||||
if self.fs_event_providers.autocmd then
|
||||
if self.realpath_root then
|
||||
-- receive notifications on BufWritePost.
|
||||
table.insert(M.autocmd_watchers, self)
|
||||
log_tree.info(
|
||||
"Monitoring root-directory %s with autocmd-monitor.",
|
||||
self.root
|
||||
)
|
||||
else
|
||||
log_tree.error(
|
||||
"Could not resolve realpath for root %s, not enabling autocmd-monitor",
|
||||
self.root
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if self.realpath_root then
|
||||
table.insert(M.active_watchers, self)
|
||||
end
|
||||
|
||||
-- do initial scan after starting the watcher.
|
||||
-- Scanning first, and then starting the watcher leaves a period of time
|
||||
-- where a new file may be created (after scanning, before watching), where
|
||||
-- we wont know about it.
|
||||
-- If I understand the uv-eventloop correctly, this function, `new`, will
|
||||
-- be executed completely before a callback is called, so self.files and
|
||||
-- self.dir_watchers should be populated correctly when a callback is
|
||||
-- received, even if it was received before all directories/files were
|
||||
-- added.
|
||||
-- This difference can be observed, at least on my machine, by watching a
|
||||
-- directory A, and then creating a nested directory B, and children for it
|
||||
-- in one command, ie. `mkdir -p A/B/{1,2,3,4,5,6,7,8,9}`.
|
||||
-- If the callback is registered after the scan, the latter directories
|
||||
-- (ie. 4-9) did not show up, whereas everything did work correctly if the
|
||||
-- watcher was activated before the scan.
|
||||
-- (almost everything, one directory was included in the initial scan and
|
||||
-- the watch-event, but that seems okay for our purposes)
|
||||
local files, dirs = Path.scandir(self.root)
|
||||
for _, file in ipairs(files) do
|
||||
local relpath = file:sub(#self.root + 2)
|
||||
self:new_file(relpath, file)
|
||||
end
|
||||
for _, dir in ipairs(dirs) do
|
||||
local relpath = dir:sub(#self.root + 2)
|
||||
self:new_dir(relpath, dir)
|
||||
end
|
||||
end
|
||||
|
||||
-- these functions maintain our logical view of the directory, and call
|
||||
-- callbacks when we detect a change.
|
||||
function TreeWatcher:new_file(rel, full)
|
||||
if self.files[rel] then
|
||||
-- already added
|
||||
return
|
||||
end
|
||||
|
||||
log_tree.debug("new file %s %s", rel, full)
|
||||
self.files[rel] = true
|
||||
self.callbacks.new_file(full)
|
||||
end
|
||||
function TreeWatcher:new_dir(rel, full)
|
||||
if self.dir_watchers[rel] then
|
||||
-- already added
|
||||
return
|
||||
end
|
||||
|
||||
log_tree.debug("new dir %s %s", rel, full)
|
||||
-- first do callback for this directory, then look into (and potentially do
|
||||
-- callbacks for) children.
|
||||
self.callbacks.new_dir(full)
|
||||
-- directory exists => don't need to set lazy.
|
||||
-- inherit fs_event_providers.
|
||||
self.dir_watchers[rel] = M.tree(
|
||||
full,
|
||||
self.depth - 1,
|
||||
self.callbacks,
|
||||
{ lazy = false, fs_event_providers = self.fs_event_providers }
|
||||
)
|
||||
end
|
||||
|
||||
function TreeWatcher:change_file(rel, full)
|
||||
log_tree.debug("changed file %s %s", rel, full)
|
||||
self.callbacks.change_file(full)
|
||||
end
|
||||
function TreeWatcher:change_dir(rel, full)
|
||||
log_tree.debug("changed dir %s %s", rel, full)
|
||||
self.callbacks.change_dir(full)
|
||||
end
|
||||
function TreeWatcher:change_child(rel, full)
|
||||
if self.dir_watchers[rel] then
|
||||
self:change_dir(rel, full)
|
||||
elseif self.files[rel] then
|
||||
self:change_file(rel, full)
|
||||
end
|
||||
end
|
||||
|
||||
function TreeWatcher:remove_child(rel, full)
|
||||
if self.dir_watchers[rel] then
|
||||
log_tree.debug("removing dir %s %s", rel, full)
|
||||
-- should have been stopped by the watcher for the child, or it was not
|
||||
-- even started due to depth.
|
||||
self.dir_watchers[rel]:remove_root()
|
||||
self.dir_watchers[rel] = nil
|
||||
|
||||
self.callbacks.remove_dir(full)
|
||||
elseif self.files[rel] then
|
||||
log_tree.debug("removing file %s %s", rel, full)
|
||||
self.files[rel] = nil
|
||||
|
||||
self.callbacks.remove_file(full)
|
||||
end
|
||||
end
|
||||
|
||||
function TreeWatcher:remove_root()
|
||||
if self.removed then
|
||||
-- already removed
|
||||
return
|
||||
end
|
||||
log_tree.debug("removing root %s", self.root)
|
||||
self.removed = true
|
||||
-- stop own, children should have handled themselves, if they are watched
|
||||
-- (and we don't need to do anything for unwatched children).
|
||||
self:stop_self()
|
||||
|
||||
-- removing entries (set them to nil) is apparently fine when iterating via
|
||||
-- pairs.
|
||||
for relpath, _ in pairs(self.files) do
|
||||
local child_full = Path.join(self.root, relpath)
|
||||
self:remove_child(relpath, child_full)
|
||||
end
|
||||
for relpath, _ in pairs(self.dir_watchers) do
|
||||
local child_full = Path.join(self.root, relpath)
|
||||
self:remove_child(relpath, child_full)
|
||||
end
|
||||
|
||||
self.callbacks.remove_root(self.root)
|
||||
end
|
||||
|
||||
--- Set up new watcher for a tree of files and directories.
|
||||
--- @param root string Absolute path to the root.
|
||||
--- @param depth number The depth up to which to monitor. 1 means that the
|
||||
--- immediate children will be monitored, 2 includes their
|
||||
--- children, and so on.
|
||||
--- @param callbacks LuaSnip.FSWatcher.TreeCallbacks The callbacks to use for this watcher.
|
||||
--- @param opts LuaSnip.FSWatcher.Options Options, described in their class.
|
||||
--- @return LuaSnip.FSWatcher.Tree
|
||||
function M.tree(root, depth, callbacks, opts)
|
||||
local lazy, fs_event_providers = get_opts(opts)
|
||||
|
||||
-- do nothing on missing callback.
|
||||
callbacks = setmetatable(callbacks or {}, callback_mt)
|
||||
|
||||
local o = setmetatable({
|
||||
root = root,
|
||||
fs_event = uv.new_fs_event(),
|
||||
files = {},
|
||||
dir_watchers = {},
|
||||
-- removed: have not yet triggered the removed-callback.
|
||||
removed = false,
|
||||
|
||||
-- track whether the watcher was stopped at some point, and if it as,
|
||||
-- don't allow it to start again.
|
||||
stopped = false,
|
||||
-- whether notifications should be sent.
|
||||
-- Modified by start/stop, wait for start to send any => start out as
|
||||
-- false.
|
||||
send_notifications = false,
|
||||
|
||||
callbacks = callbacks,
|
||||
depth = depth,
|
||||
fs_event_providers = fs_event_providers,
|
||||
}, TreeWatcher_mt)
|
||||
|
||||
-- if the path does not yet exist, set watcher up s.t. it will start
|
||||
-- watching when the directory is created.
|
||||
if not uv.fs_stat(root) and lazy then
|
||||
-- root does not yet exist, need to create a watcher that notifies us
|
||||
-- of its creation.
|
||||
local parent_path = Path.parent(root)
|
||||
if not parent_path then
|
||||
error(("Could not find parent-path for %s"):format(root))
|
||||
end
|
||||
|
||||
log_tree.info(
|
||||
"Path %s does not exist yet, watching %s for creation.",
|
||||
root,
|
||||
parent_path
|
||||
)
|
||||
|
||||
local parent_watcher
|
||||
parent_watcher = M.tree(parent_path, 1, {
|
||||
new_dir = function(full)
|
||||
if full == root then
|
||||
o:start()
|
||||
-- directory was created, stop watching.
|
||||
parent_watcher:stop_self()
|
||||
end
|
||||
end,
|
||||
-- use same providers.
|
||||
}, { lazy = true, fs_event_providers = fs_event_providers })
|
||||
else
|
||||
o:start()
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
--- @class LuaSnip.FSWatcher.Path
|
||||
--- @field private path string
|
||||
--- @field private fs_event userdata
|
||||
--- @field private removed boolean
|
||||
--- @field private stopped boolean
|
||||
--- @field private send_notifications boolean
|
||||
--- @field private callbacks LuaSnip.FSWatcher.TreeCallbacks
|
||||
--- @field private fs_event_providers table<LuaSnip.FSWatcher.FSEventProviders, boolean>
|
||||
--- @field private realpath string? Set as soon as the watcher is started.
|
||||
local PathWatcher = {}
|
||||
|
||||
local PathWatcher_mt = {
|
||||
__index = PathWatcher,
|
||||
}
|
||||
|
||||
function PathWatcher:change(full)
|
||||
log_path.info("detected change at path %s", full)
|
||||
if self.removed then
|
||||
-- this is certainly unexpected.
|
||||
log_path.warn(
|
||||
"PathWatcher at %s detected change, but path does not exist logically. Not triggering callback.",
|
||||
full
|
||||
)
|
||||
else
|
||||
self.callbacks.change(self.path)
|
||||
end
|
||||
end
|
||||
|
||||
function PathWatcher:add()
|
||||
if not self.removed then
|
||||
-- already added
|
||||
return
|
||||
end
|
||||
log_path.info("adding path %s", self.path)
|
||||
self.removed = false
|
||||
|
||||
self.callbacks.add(self.path)
|
||||
end
|
||||
function PathWatcher:remove()
|
||||
if self.removed then
|
||||
-- already removed
|
||||
return
|
||||
end
|
||||
log_path.debug("removing path %s", self.path)
|
||||
log_path.info("path %s was removed, stopping watcher.", self.path)
|
||||
|
||||
self.removed = true
|
||||
|
||||
self.callbacks.remove(self.path)
|
||||
|
||||
-- Would have to re-register for new file to receive new notifications.
|
||||
self:stop()
|
||||
end
|
||||
|
||||
function PathWatcher:fs_event_callback(err, relpath, events)
|
||||
if not self.send_notifications then
|
||||
return
|
||||
end
|
||||
|
||||
vim.schedule_wrap(function()
|
||||
log_path.debug(
|
||||
"raw: path: %s; err: %s; relpath: %s; change: %s; rename: %s",
|
||||
self.path,
|
||||
err,
|
||||
relpath,
|
||||
events.change,
|
||||
events.rename
|
||||
)
|
||||
|
||||
if events.rename then
|
||||
if not uv.fs_stat(self.path) then
|
||||
self:remove()
|
||||
else
|
||||
self:add()
|
||||
end
|
||||
elseif events.change then
|
||||
self:change()
|
||||
end
|
||||
end)()
|
||||
end
|
||||
|
||||
function PathWatcher:BufWritePost_callback(realpath)
|
||||
if not self.send_notifications then
|
||||
return
|
||||
end
|
||||
|
||||
if realpath == self.realpath then
|
||||
-- notify using passed path, not realpath.
|
||||
self:change(self.path)
|
||||
end
|
||||
end
|
||||
|
||||
function PathWatcher:start()
|
||||
if self.stopped then
|
||||
-- stop() prevents start.
|
||||
return
|
||||
end
|
||||
self.send_notifications = true
|
||||
|
||||
if self.fs_event_providers.libuv then
|
||||
-- does not work on nfs-drive, at least if it's edited from another
|
||||
-- machine.
|
||||
local success, err = self.fs_event:start(
|
||||
self.path,
|
||||
{},
|
||||
function(err, relpath, events)
|
||||
self:fs_event_callback(err, relpath, events)
|
||||
end
|
||||
)
|
||||
|
||||
if not success then
|
||||
log_path.error(
|
||||
"Could not start libuv-monitor for file %s due to error %s",
|
||||
self.path,
|
||||
err
|
||||
)
|
||||
else
|
||||
log_path.info("Monitoring file %s with libuv-monitor.", self.path)
|
||||
end
|
||||
end
|
||||
|
||||
local realpath = Path.normalize(self.path)
|
||||
|
||||
if self.fs_event_providers.autocmd then
|
||||
if realpath then
|
||||
self.realpath = realpath
|
||||
|
||||
-- path exists, add file-monitor.
|
||||
table.insert(M.autocmd_watchers, self)
|
||||
log_path.info("Monitoring file %s with autocmd-monitor.", self.path)
|
||||
else
|
||||
log_path.error(
|
||||
"Could not resolve realpath for file %s, not enabling BufWritePost-monitor",
|
||||
self.path
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if realpath then
|
||||
table.insert(M.active_watchers, self)
|
||||
end
|
||||
|
||||
if realpath then
|
||||
-- path exists, notify.
|
||||
self:add()
|
||||
-- no else, never added the path, never call remove.
|
||||
end
|
||||
end
|
||||
|
||||
function PathWatcher:stop()
|
||||
-- don't check which fs_event_providers were actually started, for both of
|
||||
-- these it should not matter if they weren't.
|
||||
self.stopped = true
|
||||
self.send_notifications = false
|
||||
|
||||
self.fs_event:stop()
|
||||
end
|
||||
|
||||
--- Set up new watcher on a single path only.
|
||||
--- @param path string Absolute path to the root.
|
||||
--- @param callbacks LuaSnip.FSWatcher.PathCallbacks The callbacks to use for this watcher.
|
||||
--- @param opts LuaSnip.FSWatcher.Options? Options, described in their class.
|
||||
--- @return LuaSnip.FSWatcher.Path
|
||||
function M.path(path, callbacks, opts)
|
||||
local lazy, fs_event_providers = get_opts(opts)
|
||||
|
||||
-- do nothing on missing callback.
|
||||
callbacks = setmetatable(callbacks or {}, callback_mt)
|
||||
|
||||
--- @as LuaSnip.FSWatcher.Path
|
||||
local o = setmetatable({
|
||||
path = path,
|
||||
fs_event = uv.new_fs_event(),
|
||||
-- Don't send an initial remove-callback if the path does not yet
|
||||
-- exist.
|
||||
-- Always send add first, or send nothing.
|
||||
removed = true,
|
||||
-- these two are just like in TreeWatcher.
|
||||
stopped = false,
|
||||
-- wait for `start()` to send notifications.
|
||||
send_notifications = false,
|
||||
callbacks = callbacks,
|
||||
fs_event_providers = fs_event_providers,
|
||||
}, PathWatcher_mt)
|
||||
|
||||
-- if the path does not yet exist, set watcher up s.t. it will start
|
||||
-- watching when the directory is created.
|
||||
if not uv.fs_stat(path) and lazy then
|
||||
-- root does not yet exist, need to create a watcher that notifies us
|
||||
-- of its creation.
|
||||
local parent_path = Path.parent(path)
|
||||
if not parent_path then
|
||||
error(("Could not find parent-path for %s"):format(path))
|
||||
end
|
||||
|
||||
log_path.info(
|
||||
"Path %s does not exist yet, watching %s for creation.",
|
||||
path,
|
||||
parent_path
|
||||
)
|
||||
|
||||
local parent_watcher
|
||||
parent_watcher = M.tree(parent_path, 1, {
|
||||
-- in path_watcher, watch for new file.
|
||||
new_file = function(full)
|
||||
log_path.info("Path: %s %s", full, path)
|
||||
if full == path then
|
||||
o:start()
|
||||
-- directory was created, stop watching.
|
||||
parent_watcher:stop_self()
|
||||
end
|
||||
end,
|
||||
}, { lazy = true, fs_event_providers = fs_event_providers })
|
||||
else
|
||||
o:start()
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,158 @@
|
||||
local util = require("luasnip.util.util")
|
||||
local Path = require("luasnip.util.path")
|
||||
local loader_util = require("luasnip.loaders.util")
|
||||
local session = require("luasnip.session")
|
||||
local loader_data = require("luasnip.loaders.data")
|
||||
local fs_watchers = require("luasnip.loaders.fs_watchers")
|
||||
local log = require("luasnip.util.log").new("loader")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- used to map cache-name to name passed to format.
|
||||
local clean_name = {
|
||||
vscode_packages = "vscode",
|
||||
vscode_standalone = "vscode-standalone",
|
||||
snipmate = "snipmate",
|
||||
lua = "lua",
|
||||
}
|
||||
local function default_format(path, _)
|
||||
path = path:gsub(
|
||||
vim.pesc(vim.fn.stdpath("data") .. "/site/pack/packer/start"),
|
||||
"$PLUGINS"
|
||||
)
|
||||
if vim.env.HOME then
|
||||
path = path:gsub(vim.pesc(vim.env.HOME .. "/.config/nvim"), "$CONFIG")
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
local function default_edit(file)
|
||||
vim.cmd("edit " .. file)
|
||||
end
|
||||
|
||||
--- Quickly jump to snippet-file from any source for the active filetypes.
|
||||
---@param opts nil|table, options for this function:
|
||||
--- - ft_filter: fn(filetype:string) -> bool
|
||||
--- Optionally filter filetypes which can be picked from. `true` -> filetype
|
||||
--- is listed, `false` -> not listed.
|
||||
---
|
||||
--- - format: fn(path:string, source_name:string) -> string|nil
|
||||
--- source_name is one of "vscode", "snipmate" or "lua".
|
||||
--- May be used to format the displayed items. For example, replace the
|
||||
--- excessively long packer-path with something shorter.
|
||||
--- If format returns nil for some item, the item will not be displayed.
|
||||
---
|
||||
--- - edit: fn(file:string): this function is called with the snippet-file as
|
||||
--- the lone argument.
|
||||
--- The default is a function which just calls `vim.cmd("edit " .. file)`.
|
||||
function M.edit_snippet_files(opts)
|
||||
opts = opts or {}
|
||||
local format = opts.format or default_format
|
||||
local edit = opts.edit or default_edit
|
||||
local extend = opts.extend or function(_, _)
|
||||
return {}
|
||||
end
|
||||
|
||||
local function ft_edit_picker(ft, _)
|
||||
if ft then
|
||||
local ft_paths = {}
|
||||
local items = {}
|
||||
|
||||
-- concat files from all loaders for the selected filetype ft.
|
||||
for cache_name, ft_file_set in pairs({
|
||||
vscode_packages = loader_data.vscode_ft_paths[ft],
|
||||
vscode_standalone = {},
|
||||
snipmate = loader_data.snipmate_ft_paths[ft],
|
||||
lua = loader_data.lua_ft_paths[ft],
|
||||
}) do
|
||||
for path, _ in pairs(ft_file_set or {}) do
|
||||
local fmt_name = format(path, clean_name[cache_name])
|
||||
if fmt_name then
|
||||
table.insert(ft_paths, path)
|
||||
table.insert(items, fmt_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- extend filetypes with user-defined function.
|
||||
local extended = extend(ft, ft_paths)
|
||||
assert(
|
||||
type(extended) == "table",
|
||||
"You must return a table in extend function"
|
||||
)
|
||||
for _, pair in ipairs(extended) do
|
||||
table.insert(items, pair[1])
|
||||
table.insert(ft_paths, pair[2])
|
||||
end
|
||||
|
||||
-- prompt user again if there are multiple files providing this filetype.
|
||||
if #ft_paths > 1 then
|
||||
vim.ui.select(items, {
|
||||
prompt = "Multiple files for this filetype, choose one:",
|
||||
}, function(_, indx)
|
||||
if indx and ft_paths[indx] then
|
||||
edit(ft_paths[indx])
|
||||
end
|
||||
end)
|
||||
elseif ft_paths[1] then
|
||||
edit(ft_paths[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ft_filter = opts.ft_filter or util.yes
|
||||
|
||||
local all_fts = {}
|
||||
vim.list_extend(all_fts, util.get_snippet_filetypes())
|
||||
vim.list_extend(
|
||||
all_fts,
|
||||
loader_util.get_load_fts(vim.api.nvim_get_current_buf())
|
||||
)
|
||||
all_fts = util.deduplicate(all_fts)
|
||||
|
||||
local filtered_fts = {}
|
||||
for _, ft in ipairs(all_fts) do
|
||||
if ft_filter(ft) then
|
||||
table.insert(filtered_fts, ft)
|
||||
end
|
||||
end
|
||||
|
||||
if #filtered_fts == 1 then
|
||||
ft_edit_picker(filtered_fts[1])
|
||||
elseif #filtered_fts > 1 then
|
||||
vim.ui.select(filtered_fts, {
|
||||
prompt = "Select filetype:",
|
||||
}, ft_edit_picker)
|
||||
end
|
||||
end
|
||||
|
||||
function M.cleanup()
|
||||
require("luasnip.loaders.from_lua").clean()
|
||||
require("luasnip.loaders.from_snipmate").clean()
|
||||
require("luasnip.loaders.from_vscode").clean()
|
||||
end
|
||||
|
||||
function M.load_lazy_loaded(bufnr)
|
||||
local fts = loader_util.get_load_fts(bufnr)
|
||||
|
||||
for _, ft in ipairs(fts) do
|
||||
if not session.loaded_fts[ft] then
|
||||
require("luasnip.loaders.from_lua")._load_lazy_loaded_ft(ft)
|
||||
require("luasnip.loaders.from_snipmate")._load_lazy_loaded_ft(ft)
|
||||
require("luasnip.loaders.from_vscode")._load_lazy_loaded_ft(ft)
|
||||
end
|
||||
session.loaded_fts[ft] = true
|
||||
end
|
||||
end
|
||||
|
||||
function M.reload_file(path)
|
||||
local realpath = Path.normalize(path)
|
||||
if not realpath then
|
||||
return nil, ("Could not reload file %s: does not exist."):format(path)
|
||||
else
|
||||
fs_watchers.write_notify(realpath)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,93 @@
|
||||
local uv = vim.uv or vim.loop
|
||||
local duplicate = require("luasnip.nodes.duplicate")
|
||||
|
||||
--- @class LuaSnip.Loaders.SnippetCache.Mtime
|
||||
--- @field sec number
|
||||
--- @field nsec number
|
||||
--- Stores modified time for a file.
|
||||
|
||||
--- @class LuaSnip.Loaders.SnippetCache.TimeCacheEntry
|
||||
--- @field mtime LuaSnip.Loaders.SnippetCache.Mtime?
|
||||
--- @field data LuaSnip.Loaders.SnippetFileData
|
||||
--- mtime is nil if the file does not currently exist. Since `get_fn` may still
|
||||
--- return data, there's no need to treat this differently.
|
||||
|
||||
--- @class LuaSnip.Loaders.SnippetCache
|
||||
--- SnippetCache stores snippets and other data loaded by files.
|
||||
--- @field private get_fn fun(file: string): LuaSnip.Loaders.SnippetFileData
|
||||
--- @field private cache table<string, LuaSnip.Loaders.SnippetCache.TimeCacheEntry>
|
||||
local SnippetCache = {}
|
||||
SnippetCache.__index = SnippetCache
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @class LuaSnip.Loaders.SnippetFileData
|
||||
--- @field snippets LuaSnip.Addable[]
|
||||
--- @field autosnippets LuaSnip.Addable[]
|
||||
--- @field misc table any data.
|
||||
|
||||
--- Create new cache.
|
||||
--- @param get_fn fun(file: string): LuaSnip.Loaders.SnippetFileData
|
||||
--- @return LuaSnip.Loaders.SnippetCache
|
||||
function M.new(get_fn)
|
||||
return setmetatable({
|
||||
get_fn = get_fn,
|
||||
cache = {},
|
||||
}, SnippetCache)
|
||||
end
|
||||
|
||||
--- Copy addables from data to new table.
|
||||
--- @param data LuaSnip.Loaders.SnippetFileData
|
||||
--- @return LuaSnip.Loaders.SnippetFileData
|
||||
local function copy_filedata(data)
|
||||
--- @as LuaSnip.Loaders.SnippetFileData
|
||||
return {
|
||||
snippets = vim.tbl_map(duplicate.duplicate_addable, data.snippets),
|
||||
autosnippets = vim.tbl_map(
|
||||
duplicate.duplicate_addable,
|
||||
data.autosnippets
|
||||
),
|
||||
misc = vim.deepcopy(data.misc),
|
||||
}
|
||||
end
|
||||
|
||||
--- Retrieve loaded data for any file, either from the cache, or directly from
|
||||
--- the file.
|
||||
--- For storage-efficiency (and to elide the otherwise necessary deepcopy), the
|
||||
--- snippets are duplicated, which should not leak.
|
||||
--- @param fname string
|
||||
--- @return LuaSnip.Loaders.SnippetFileData
|
||||
function SnippetCache:fetch(fname)
|
||||
local cached = self.cache[fname]
|
||||
local current_stat = uv.fs_stat(fname)
|
||||
|
||||
--- @as LuaSnip.Loaders.SnippetCache.Mtime
|
||||
local mtime = current_stat and current_stat.mtime
|
||||
|
||||
if
|
||||
cached
|
||||
and mtime
|
||||
and mtime.sec == cached.mtime.sec
|
||||
and mtime.nsec == cached.mtime.nsec
|
||||
then
|
||||
-- happy path: data is cached, and valid => just return cached data.
|
||||
return copy_filedata(cached.data)
|
||||
end
|
||||
|
||||
-- data is stale (cache entry does not exist, file was written after
|
||||
-- cache-creation, or the file was deleted).
|
||||
-- fetch data from updated file
|
||||
local res = self.get_fn(fname)
|
||||
|
||||
-- store it.
|
||||
self.cache[fname] = {
|
||||
data = res,
|
||||
mtime = mtime,
|
||||
}
|
||||
|
||||
-- return it.
|
||||
-- Don't copy here, no need to.
|
||||
return res
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,8 @@
|
||||
--- @class LuaSnip.Loaders.LoadOpts
|
||||
--- @field paths string[]?|string? Either a list of paths, or ","-delimited paths. If nil, searches rtp for snippet-collections.
|
||||
--- @field lazy_paths string[]?|string? Like paths, but these will be watched, and loaded when creation is detected.
|
||||
--- @field include string[]? If set, all filetypes not in include will be excluded from loading.
|
||||
--- @field exclude string[]? Exclude these filetypes, even if they are set in include.
|
||||
--- @field override_priority number? load all snippets with this priority.
|
||||
--- @field default_priority number? snippet-priority, unless the snippet sets its own priority.
|
||||
--- @field fs_event_providers table<LuaSnip.FSWatcher.FSEventProviders, boolean>? How to monitor the filesystem
|
||||
@ -0,0 +1,299 @@
|
||||
local Path = require("luasnip.util.path")
|
||||
local util = require("luasnip.util.util")
|
||||
local session = require("luasnip.session")
|
||||
local snippet_collection = require("luasnip.session.snippet_collection")
|
||||
local log = require("luasnip.util.log").new("loaders")
|
||||
|
||||
local function filetypelist_to_set(list)
|
||||
vim.validate({ list = { list, "table", true } })
|
||||
if not list then
|
||||
return list
|
||||
end
|
||||
local out = {}
|
||||
for _, ft in ipairs(list) do
|
||||
out[ft] = true
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function split_lines(filestring)
|
||||
local newline_code
|
||||
if vim.endswith(filestring, "\r\n") then -- dos
|
||||
newline_code = "\r\n"
|
||||
elseif vim.endswith(filestring, "\r") then -- mac
|
||||
-- both mac and unix-files contain a trailing newline which would lead
|
||||
-- to an additional empty line being read (\r, \n _terminate_ lines, they
|
||||
-- don't _separate_ them)
|
||||
newline_code = "\r"
|
||||
filestring = filestring:sub(1, -2)
|
||||
elseif vim.endswith(filestring, "\n") then -- unix
|
||||
newline_code = "\n"
|
||||
filestring = filestring:sub(1, -2)
|
||||
else -- dos
|
||||
newline_code = "\r\n"
|
||||
end
|
||||
return vim.split(
|
||||
filestring,
|
||||
newline_code,
|
||||
{ plain = true, trimemtpy = false }
|
||||
)
|
||||
end
|
||||
|
||||
local function non_nil(v)
|
||||
return v ~= nil
|
||||
end
|
||||
|
||||
local function resolve_root_paths(paths, rtp_dirname)
|
||||
if not paths then
|
||||
paths = vim.api.nvim_get_runtime_file(rtp_dirname, true)
|
||||
end
|
||||
|
||||
paths = vim.tbl_map(Path.expand, paths)
|
||||
paths = vim.tbl_filter(non_nil, paths)
|
||||
paths = util.deduplicate(paths)
|
||||
|
||||
return paths
|
||||
end
|
||||
|
||||
local function resolve_lazy_root_paths(paths)
|
||||
paths = vim.tbl_map(Path.expand_maybe_nonexisting, paths)
|
||||
paths = vim.tbl_filter(non_nil, paths)
|
||||
paths = util.deduplicate(paths)
|
||||
|
||||
return paths
|
||||
end
|
||||
|
||||
local function ft_filter(include, exclude)
|
||||
exclude = filetypelist_to_set(exclude)
|
||||
include = filetypelist_to_set(include)
|
||||
|
||||
return function(lang)
|
||||
if exclude and exclude[lang] then
|
||||
return false
|
||||
end
|
||||
if include == nil or include[lang] then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _append(tbl, name, elem)
|
||||
if tbl[name] == nil then
|
||||
tbl[name] = {}
|
||||
end
|
||||
table.insert(tbl[name], elem)
|
||||
end
|
||||
|
||||
---Get paths of .snippets files
|
||||
---@param root string @snippet directory path
|
||||
---@return table @keys are file types, values are paths
|
||||
local function get_ft_paths(root, extension)
|
||||
local ft_path = {}
|
||||
local files, dirs = Path.scandir(root)
|
||||
for _, file in ipairs(files) do
|
||||
local ft, ext = Path.basename(file, true)
|
||||
if ext == extension then
|
||||
_append(ft_path, ft, file)
|
||||
end
|
||||
end
|
||||
for _, dir in ipairs(dirs) do
|
||||
-- directory-name is ft for snippet-files.
|
||||
local ft = vim.fn.fnamemodify(dir, ":t")
|
||||
files, _ = Path.scandir(dir)
|
||||
for _, file in ipairs(files) do
|
||||
if vim.endswith(file, extension) then
|
||||
-- produce normalized filenames.
|
||||
local normalized_fname = Path.normalize(file)
|
||||
if normalized_fname then
|
||||
_append(ft_path, ft, normalized_fname)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return ft_path
|
||||
end
|
||||
|
||||
-- fname must be in the directory-tree below root.
|
||||
-- collection_root may not end with a path-separator.
|
||||
-- If both are from "realpath", and fname belongs to the collection, this
|
||||
-- should be a given.
|
||||
local function collection_file_ft(collection_root, fname)
|
||||
local collection_components = Path.components(collection_root)
|
||||
local fname_components = Path.components(fname)
|
||||
|
||||
if #fname_components == #collection_components + 1 then
|
||||
-- if the file is a direct child of the collection-root, get the text
|
||||
-- before the last dot.
|
||||
return fname_components[#collection_components + 1]:match(
|
||||
"(.*)%.[^%.]*"
|
||||
)
|
||||
else
|
||||
-- if the file is nested deeper, the name of the directory immediately
|
||||
-- below the root is the filetype.
|
||||
return fname_components[#collection_components + 1]
|
||||
end
|
||||
end
|
||||
|
||||
-- extend table like {lua = {path1}, c = {path1, path2}, ...}, new_paths has the same layout.
|
||||
local function extend_ft_paths(paths, new_paths)
|
||||
for ft, path in pairs(new_paths) do
|
||||
if paths[ft] then
|
||||
vim.list_extend(paths[ft], path)
|
||||
else
|
||||
paths[ft] = vim.deepcopy(path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Find
|
||||
--- 1. all files that belong to a collection and
|
||||
--- 2. the files from that
|
||||
--- collection that should actually be loaded.
|
||||
---@param opts table: straight from `load`/`lazy_load`.
|
||||
---@param rtp_dirname string: if no path is given in opts, we look for a
|
||||
--- directory named `rtp_dirname` in the runtimepath.
|
||||
---@param extension string: extension of valid snippet-files for the given
|
||||
--- collection (eg `.lua` or `.snippets`)
|
||||
---@return table: a list of tables, each of the inner tables contains two
|
||||
--- entries:
|
||||
--- - collection_paths: ft->files for the entire collection and
|
||||
--- - load_paths: ft->files for only the files that should be loaded.
|
||||
--- All produced filenames are normalized, eg. links are resolved and
|
||||
--- unnecessary . or .. removed.
|
||||
local function get_load_paths_snipmate_like(opts, rtp_dirname, extension)
|
||||
local collections_load_paths = {}
|
||||
|
||||
for _, path in ipairs(resolve_root_paths(opts.paths, rtp_dirname)) do
|
||||
local collection_ft_paths = get_ft_paths(path, extension)
|
||||
|
||||
local load_paths = vim.deepcopy(collection_ft_paths)
|
||||
-- remove files for excluded/non-included filetypes here.
|
||||
local collection_filter = ft_filter(opts.exclude, opts.include)
|
||||
for ft, _ in pairs(load_paths) do
|
||||
if not collection_filter(ft) then
|
||||
load_paths[ft] = nil
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(collections_load_paths, {
|
||||
collection_paths = collection_ft_paths,
|
||||
load_paths = load_paths,
|
||||
})
|
||||
end
|
||||
|
||||
return collections_load_paths
|
||||
end
|
||||
|
||||
--- Asks (via vim.ui.select) to edit a file that currently provides snippets
|
||||
---@param ft_files table, map filetype to a number of files.
|
||||
local function edit_snippet_files(ft_files)
|
||||
local fts = util.get_snippet_filetypes()
|
||||
vim.ui.select(fts, {
|
||||
prompt = "Select filetype:",
|
||||
}, function(item, _)
|
||||
if item then
|
||||
local ft_paths = ft_files[item]
|
||||
if ft_paths then
|
||||
-- prompt user again if there are multiple files providing this filetype.
|
||||
if #ft_paths > 1 then
|
||||
vim.ui.select(ft_paths, {
|
||||
prompt = "Multiple files for this filetype, choose one:",
|
||||
}, function(multi_item)
|
||||
if multi_item then
|
||||
vim.cmd("edit " .. multi_item)
|
||||
end
|
||||
end)
|
||||
else
|
||||
vim.cmd("edit " .. ft_paths[1])
|
||||
end
|
||||
else
|
||||
print("No file for this filetype.")
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function make_add_opts(opts)
|
||||
return {
|
||||
override_priority = opts.override_priority,
|
||||
default_priority = opts.default_priority,
|
||||
}
|
||||
end
|
||||
|
||||
local function get_load_fts(bufnr)
|
||||
local fts = session.config.load_ft_func(bufnr)
|
||||
-- also add "all", loaded by all buffers.
|
||||
table.insert(fts, "all")
|
||||
|
||||
return util.deduplicate(util.redirect_filetypes(fts))
|
||||
end
|
||||
|
||||
local function add_file_snippets(ft, filename, snippets, autosnippets, add_opts)
|
||||
snippet_collection.add_snippets(
|
||||
{ [ft] = snippets },
|
||||
vim.tbl_extend("keep", {
|
||||
type = "snippets",
|
||||
key = "__snippets__" .. ft .. "__" .. filename,
|
||||
}, add_opts)
|
||||
)
|
||||
snippet_collection.add_snippets(
|
||||
{ [ft] = autosnippets },
|
||||
vim.tbl_extend("keep", {
|
||||
type = "autosnippets",
|
||||
key = "__autosnippets__" .. ft .. "__" .. filename,
|
||||
}, add_opts)
|
||||
)
|
||||
log.info(
|
||||
"Adding %s snippets and %s autosnippets from %s to ft `%s`",
|
||||
#snippets,
|
||||
#autosnippets,
|
||||
filename,
|
||||
ft
|
||||
)
|
||||
end
|
||||
|
||||
local function normalize_opts(opts)
|
||||
opts = opts or {}
|
||||
|
||||
local paths = opts.paths
|
||||
if type(paths) == "string" then
|
||||
paths = vim.split(paths, ",")
|
||||
end
|
||||
|
||||
local add_opts = make_add_opts(opts)
|
||||
local include = opts.include
|
||||
local exclude = opts.exclude
|
||||
local lazy_paths = opts.lazy_paths or {}
|
||||
if type(lazy_paths) == "string" then
|
||||
lazy_paths = vim.split(lazy_paths, ",")
|
||||
end
|
||||
|
||||
local fs_event_providers =
|
||||
vim.F.if_nil(opts.fs_event_providers, { autocmd = true, libuv = false })
|
||||
|
||||
return {
|
||||
paths = paths,
|
||||
lazy_paths = lazy_paths,
|
||||
include = include,
|
||||
exclude = exclude,
|
||||
add_opts = add_opts,
|
||||
fs_event_providers = fs_event_providers,
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
filetypelist_to_set = filetypelist_to_set,
|
||||
split_lines = split_lines,
|
||||
resolve_root_paths = resolve_root_paths,
|
||||
resolve_lazy_root_paths = resolve_lazy_root_paths,
|
||||
ft_filter = ft_filter,
|
||||
get_ft_paths = get_ft_paths,
|
||||
get_load_paths_snipmate_like = get_load_paths_snipmate_like,
|
||||
extend_ft_paths = extend_ft_paths,
|
||||
edit_snippet_files = edit_snippet_files,
|
||||
make_add_opts = make_add_opts,
|
||||
collection_file_ft = collection_file_ft,
|
||||
get_load_fts = get_load_fts,
|
||||
add_file_snippets = add_file_snippets,
|
||||
normalize_opts = normalize_opts,
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
-- absolute_indexer[0][1][2][3] -> { absolute_insert_position = {0,1,2,3} }
|
||||
|
||||
local function new()
|
||||
return setmetatable({
|
||||
absolute_insert_position = {},
|
||||
}, {
|
||||
__index = function(table, key)
|
||||
table.absolute_insert_position[#table.absolute_insert_position + 1] =
|
||||
key
|
||||
return table
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return setmetatable({}, {
|
||||
__index = function(_, key)
|
||||
-- create new table and index it.
|
||||
return new()[key]
|
||||
end,
|
||||
__call = function(_, ...)
|
||||
return {
|
||||
-- passing ... to a function passes only the first of the
|
||||
-- variable number of args.
|
||||
absolute_insert_position = type(...) == "number" and { ... } or ...,
|
||||
}
|
||||
end,
|
||||
})
|
||||
@ -0,0 +1,427 @@
|
||||
local Node = require("luasnip.nodes.node").Node
|
||||
local ChoiceNode = Node:new()
|
||||
local util = require("luasnip.util.util")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local types = require("luasnip.util.types")
|
||||
local events = require("luasnip.util.events")
|
||||
local mark = require("luasnip.util.mark").mark
|
||||
local session = require("luasnip.session")
|
||||
local sNode = require("luasnip.nodes.snippet").SN
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
|
||||
function ChoiceNode:init_nodes()
|
||||
for i, choice in ipairs(self.choices) do
|
||||
-- setup jumps
|
||||
choice.next = self
|
||||
choice.prev = self
|
||||
|
||||
-- forward values for unknown keys from choiceNode.
|
||||
choice.choice = self
|
||||
local node_mt = getmetatable(choice)
|
||||
setmetatable(choice, {
|
||||
__index = function(node, key)
|
||||
return node_mt[key] or node.choice[key]
|
||||
end,
|
||||
})
|
||||
|
||||
-- replace nodes' original update_dependents with function that also
|
||||
-- calls this choiceNodes' update_dependents.
|
||||
--
|
||||
-- cannot define as `function node:update_dependents()` as _this_
|
||||
-- choiceNode would be `self`.
|
||||
-- Also rely on node.choice, as using `self` there wouldn't be caught
|
||||
-- by copy and the wrong node would be updated.
|
||||
choice.update_dependents = function(node)
|
||||
node:_update_dependents()
|
||||
node.choice:update_dependents()
|
||||
end
|
||||
|
||||
choice.next_choice = self.choices[i + 1]
|
||||
choice.prev_choice = self.choices[i - 1]
|
||||
end
|
||||
self.choices[#self.choices].next_choice = self.choices[1]
|
||||
self.choices[1].prev_choice = self.choices[#self.choices]
|
||||
|
||||
self.active_choice = self.choices[1]
|
||||
end
|
||||
|
||||
local function C(pos, choices, opts)
|
||||
opts = opts or {}
|
||||
if opts.restore_cursor == nil then
|
||||
-- disable by default, can affect performance.
|
||||
opts.restore_cursor = false
|
||||
end
|
||||
|
||||
-- allow passing table of nodes in choices, will be turned into a
|
||||
-- snippetNode.
|
||||
for indx, choice in ipairs(choices) do
|
||||
if not getmetatable(choice) then
|
||||
-- is a normal table, not a node.
|
||||
choices[indx] = sNode(nil, choice)
|
||||
end
|
||||
end
|
||||
|
||||
local c = ChoiceNode:new({
|
||||
active = false,
|
||||
pos = pos,
|
||||
choices = choices,
|
||||
type = types.choiceNode,
|
||||
mark = nil,
|
||||
dependents = {},
|
||||
-- default to true.
|
||||
restore_cursor = opts.restore_cursor,
|
||||
}, opts)
|
||||
c:init_nodes()
|
||||
return c
|
||||
end
|
||||
extend_decorator.register(C, { arg_indx = 3 })
|
||||
|
||||
function ChoiceNode:subsnip_init()
|
||||
node_util.subsnip_init_children(self.parent, self.choices)
|
||||
end
|
||||
|
||||
ChoiceNode.init_positions = node_util.init_child_positions_func(
|
||||
"absolute_position",
|
||||
"choices",
|
||||
"init_positions"
|
||||
)
|
||||
ChoiceNode.init_insert_positions = node_util.init_child_positions_func(
|
||||
"absolute_insert_position",
|
||||
"choices",
|
||||
"init_insert_positions"
|
||||
)
|
||||
|
||||
function ChoiceNode:make_args_absolute()
|
||||
-- relative indices are relative to the parent of the choiceNode,
|
||||
-- temporarily remove last component of position
|
||||
local last_indx = #self.absolute_insert_position
|
||||
local last = self.absolute_insert_position[last_indx]
|
||||
self.absolute_insert_position[#self.absolute_insert_position] = nil
|
||||
|
||||
for _, choice in ipairs(self.choices) do
|
||||
-- relative to choiceNode!!
|
||||
choice:make_args_absolute(self.absolute_insert_position)
|
||||
end
|
||||
|
||||
self.absolute_insert_position[last_indx] = last
|
||||
end
|
||||
|
||||
function ChoiceNode:put_initial(pos)
|
||||
local old_pos = vim.deepcopy(pos)
|
||||
|
||||
self.active_choice:put_initial(pos)
|
||||
|
||||
local mark_opts = vim.tbl_extend("keep", {
|
||||
right_gravity = false,
|
||||
end_right_gravity = false,
|
||||
}, self.active_choice:get_passive_ext_opts())
|
||||
|
||||
self.active_choice.mark = mark(old_pos, pos, mark_opts)
|
||||
self.visible = true
|
||||
end
|
||||
|
||||
function ChoiceNode:indent(indentstr)
|
||||
for _, node in ipairs(self.choices) do
|
||||
node:indent(indentstr)
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceNode:expand_tabs(tabwidth, indentstringlen)
|
||||
for _, node in ipairs(self.choices) do
|
||||
node:expand_tabs(tabwidth, indentstringlen)
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceNode:input_enter(_, dry_run)
|
||||
if dry_run then
|
||||
dry_run.active[self] = true
|
||||
return
|
||||
end
|
||||
|
||||
self.mark:update_opts(self.ext_opts.active)
|
||||
self:focus()
|
||||
|
||||
self.prev_choice_node =
|
||||
session.active_choice_nodes[vim.api.nvim_get_current_buf()]
|
||||
session.active_choice_nodes[vim.api.nvim_get_current_buf()] = self
|
||||
self.visited = true
|
||||
self.active = true
|
||||
|
||||
self:event(events.enter)
|
||||
end
|
||||
|
||||
function ChoiceNode:input_leave(_, dry_run)
|
||||
if dry_run then
|
||||
dry_run.active[self] = false
|
||||
return
|
||||
end
|
||||
|
||||
self:event(events.leave)
|
||||
|
||||
self.mark:update_opts(self:get_passive_ext_opts())
|
||||
self:update_dependents()
|
||||
session.active_choice_nodes[vim.api.nvim_get_current_buf()] =
|
||||
self.prev_choice_node
|
||||
self.active = false
|
||||
end
|
||||
|
||||
function ChoiceNode:set_old_text()
|
||||
self.old_text = self:get_text()
|
||||
self.active_choice.old_text = self.old_text
|
||||
end
|
||||
|
||||
function ChoiceNode:get_static_text()
|
||||
return self.choices[1]:get_static_text()
|
||||
end
|
||||
|
||||
function ChoiceNode:get_docstring()
|
||||
return util.string_wrap(
|
||||
self.choices[1]:get_docstring(),
|
||||
rawget(self, "pos")
|
||||
)
|
||||
end
|
||||
|
||||
function ChoiceNode:jump_into(dir, no_move, dry_run)
|
||||
self:init_dry_run_active(dry_run)
|
||||
|
||||
if self:is_active(dry_run) then
|
||||
self:input_leave(no_move, dry_run)
|
||||
|
||||
if dir == 1 then
|
||||
return self.next:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
return self.prev:jump_into(dir, no_move, dry_run)
|
||||
end
|
||||
else
|
||||
self:input_enter(no_move, dry_run)
|
||||
|
||||
return self.active_choice:jump_into(dir, no_move, dry_run)
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceNode:update()
|
||||
self.active_choice:update()
|
||||
end
|
||||
|
||||
function ChoiceNode:update_static_all()
|
||||
for _, choice in ipairs(self.choices) do
|
||||
choice:update_static()
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceNode:update_static()
|
||||
self.active_choice:update_static()
|
||||
end
|
||||
|
||||
function ChoiceNode:update_restore()
|
||||
self.active_choice:update_restore()
|
||||
end
|
||||
|
||||
function ChoiceNode:setup_choice_jumps() end
|
||||
|
||||
function ChoiceNode:find_node(predicate)
|
||||
if self.active_choice then
|
||||
if predicate(self.active_choice) then
|
||||
return self.active_choice
|
||||
else
|
||||
return self.active_choice:find_node(predicate)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- used to uniquely identify this change-choice-action.
|
||||
local change_choice_id = 0
|
||||
|
||||
function ChoiceNode:set_choice(choice, current_node)
|
||||
change_choice_id = change_choice_id + 1
|
||||
-- to uniquely identify this node later (storing the pointer isn't enough
|
||||
-- because this is supposed to work with restoreNodes, which are copied).
|
||||
current_node.change_choice_id = change_choice_id
|
||||
|
||||
local insert_pre_cc = vim.fn.mode() == "i"
|
||||
-- is byte-indexed! Doesn't matter here, but important to be aware of.
|
||||
local cursor_pos_pre_relative =
|
||||
util.pos_sub(util.get_cursor_0ind(), current_node.mark:pos_begin_raw())
|
||||
|
||||
self.active_choice:store()
|
||||
|
||||
-- tear down current choice.
|
||||
-- leave all so the choice (could be a snippet) is in the correct state for the next enter.
|
||||
node_util.leave_nodes_between(self.active_choice, current_node)
|
||||
|
||||
self.active_choice:exit()
|
||||
|
||||
-- clear text.
|
||||
--
|
||||
-- active_choice has to be disabled (nilled?) to prevent reading from
|
||||
-- cleared mark in set_mark_rgrav (which will be called in
|
||||
-- self:set_text({""}) a few lines below).
|
||||
self.active_choice = nil
|
||||
self:set_text({ "" })
|
||||
|
||||
self.active_choice = choice
|
||||
|
||||
self.active_choice.mark = self.mark:copy_pos_gravs(
|
||||
vim.deepcopy(self.active_choice:get_passive_ext_opts())
|
||||
)
|
||||
|
||||
-- re-init positions for child-restoreNodes (they will update their
|
||||
-- children in put_initial, but their own position has to be changed here).
|
||||
self:init_positions(self.absolute_position)
|
||||
self:init_insert_positions(self.absolute_insert_position)
|
||||
|
||||
-- self is still focused, from `set_text`.
|
||||
self.active_choice:put_initial(self.mark:pos_begin_raw())
|
||||
-- adjust gravity in left side of inserted node, such that it matches the
|
||||
-- current gravity of self.
|
||||
local _, to = self.mark:pos_begin_end_raw()
|
||||
self.active_choice:subtree_set_pos_rgrav(to, -1, true)
|
||||
|
||||
self.active_choice:update_restore()
|
||||
self.active_choice:update_all_dependents()
|
||||
self:update_dependents()
|
||||
|
||||
-- Another node may have been entered in update_dependents.
|
||||
self:focus()
|
||||
self:event(events.change_choice)
|
||||
|
||||
if self.restore_cursor then
|
||||
local target_node = self:find_node(function(test_node)
|
||||
return test_node.change_choice_id == change_choice_id
|
||||
end)
|
||||
|
||||
if target_node then
|
||||
-- the node that the cursor was in when changeChoice was called exists
|
||||
-- in the active choice! Enter it and all nodes between it and this choiceNode,
|
||||
-- then set the cursor.
|
||||
-- Pass no_move=true, we will set the cursor ourselves.
|
||||
node_util.enter_nodes_between(self, target_node, true)
|
||||
|
||||
if insert_pre_cc then
|
||||
util.set_cursor_0ind(
|
||||
util.pos_add(
|
||||
target_node.mark:pos_begin_raw(),
|
||||
cursor_pos_pre_relative
|
||||
)
|
||||
)
|
||||
else
|
||||
node_util.select_node(target_node)
|
||||
end
|
||||
return target_node
|
||||
end
|
||||
end
|
||||
|
||||
return self.active_choice:jump_into(1)
|
||||
end
|
||||
|
||||
function ChoiceNode:change_choice(dir, current_node)
|
||||
-- stylua: ignore
|
||||
return self:set_choice(
|
||||
dir == 1 and self.active_choice.next_choice
|
||||
or self.active_choice.prev_choice,
|
||||
current_node )
|
||||
end
|
||||
|
||||
function ChoiceNode:copy()
|
||||
local o = vim.deepcopy(self)
|
||||
for i, node in ipairs(self.choices) do
|
||||
if node.type == types.snippetNode or node.type == types.choiceNode then
|
||||
o.choices[i] = node:copy()
|
||||
else
|
||||
setmetatable(o.choices[i], getmetatable(node))
|
||||
end
|
||||
end
|
||||
setmetatable(o, getmetatable(self))
|
||||
return o
|
||||
end
|
||||
|
||||
function ChoiceNode:exit()
|
||||
self.visible = false
|
||||
if self.active_choice then
|
||||
self.active_choice:exit()
|
||||
end
|
||||
self.mark:clear()
|
||||
if self.active then
|
||||
session.active_choice_nodes[vim.api.nvim_get_current_buf()] =
|
||||
self.prev_choice_node
|
||||
end
|
||||
self.active = false
|
||||
end
|
||||
|
||||
function ChoiceNode:set_ext_opts(name)
|
||||
Node.set_ext_opts(self, name)
|
||||
|
||||
self.active_choice:set_ext_opts(name)
|
||||
end
|
||||
|
||||
function ChoiceNode:store()
|
||||
self.active_choice:store()
|
||||
end
|
||||
|
||||
function ChoiceNode:insert_to_node_absolute(position)
|
||||
if #position == 0 then
|
||||
return self.absolute_position
|
||||
end
|
||||
local front = util.pop_front(position)
|
||||
return self.choices[front]:insert_to_node_absolute(position)
|
||||
end
|
||||
|
||||
function ChoiceNode:set_dependents()
|
||||
for _, node in ipairs(self.choices) do
|
||||
node:set_dependents()
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceNode:set_argnodes(dict)
|
||||
Node.set_argnodes(self, dict)
|
||||
|
||||
for _, node in ipairs(self.choices) do
|
||||
node:set_argnodes(dict)
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceNode:update_all_dependents()
|
||||
-- call the version that only updates this node.
|
||||
self:_update_dependents()
|
||||
|
||||
self.active_choice:update_all_dependents()
|
||||
end
|
||||
|
||||
function ChoiceNode:update_all_dependents_static()
|
||||
-- call the version that only updates this node.
|
||||
self:_update_dependents_static()
|
||||
|
||||
self.active_choice:update_all_dependents_static()
|
||||
end
|
||||
|
||||
function ChoiceNode:resolve_position(position)
|
||||
return self.choices[position]
|
||||
end
|
||||
|
||||
function ChoiceNode:static_init()
|
||||
Node.static_init(self)
|
||||
self.active_choice:static_init()
|
||||
end
|
||||
|
||||
function ChoiceNode:subtree_set_pos_rgrav(pos, direction, rgrav)
|
||||
self.mark:set_rgrav(-direction, rgrav)
|
||||
if self.active_choice then
|
||||
self.active_choice:subtree_set_pos_rgrav(pos, direction, rgrav)
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceNode:subtree_set_rgrav(rgrav)
|
||||
self.mark:set_rgravs(rgrav, rgrav)
|
||||
if self.active_choice then
|
||||
self.active_choice:subtree_set_rgrav(rgrav)
|
||||
end
|
||||
end
|
||||
|
||||
function ChoiceNode:extmarks_valid()
|
||||
return node_util.generic_extmarks_valid(self, self.active_choice)
|
||||
end
|
||||
|
||||
return {
|
||||
C = C,
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
local snip_mod = require("luasnip.nodes.snippet")
|
||||
|
||||
local M = {}
|
||||
|
||||
local DupExpandable = {}
|
||||
|
||||
-- just pass these through to _expandable.
|
||||
function DupExpandable:get_docstring()
|
||||
return self._expandable:get_docstring()
|
||||
end
|
||||
function DupExpandable:copy()
|
||||
local copy = self._expandable:copy()
|
||||
copy.id = self.id
|
||||
|
||||
return copy
|
||||
end
|
||||
|
||||
-- this is modified in `self:invalidate` _and_ needs to be called on _expandable.
|
||||
function DupExpandable:matches(...)
|
||||
-- use snippet-module matches, self._expandable might have had its match
|
||||
-- overwritten by invalidate.
|
||||
-- (if there are more issues with this, consider some other mechanism for
|
||||
-- invalidating)
|
||||
return snip_mod.Snippet.matches(self._expandable, ...)
|
||||
end
|
||||
|
||||
-- invalidate has to be called on this snippet itself.
|
||||
function DupExpandable:invalidate()
|
||||
snip_mod.Snippet.invalidate(self)
|
||||
end
|
||||
|
||||
local dup_mt = {
|
||||
-- index DupExpandable for own functions, and then the expandable stored in
|
||||
-- self/t.
|
||||
__index = function(t, k)
|
||||
if DupExpandable[k] then
|
||||
return DupExpandable[k]
|
||||
end
|
||||
|
||||
return t._expandable[k]
|
||||
end,
|
||||
}
|
||||
|
||||
function M.duplicate_expandable(expandable)
|
||||
return setmetatable({
|
||||
_expandable = expandable,
|
||||
-- copy these!
|
||||
-- if `expandable` is invalidated, we don't necessarily want this
|
||||
-- expandable to be invalidated as well.
|
||||
hidden = expandable.hidden,
|
||||
invalidated = expandable.invalidated,
|
||||
}, dup_mt)
|
||||
end
|
||||
|
||||
local DupAddable = {}
|
||||
|
||||
function DupAddable:retrieve_all()
|
||||
-- always return the same set of items, necessary when for invalidate via
|
||||
-- key to work correctly.
|
||||
return self._all
|
||||
end
|
||||
local DupAddable_mt = {
|
||||
__index = DupAddable,
|
||||
}
|
||||
|
||||
function M.duplicate_addable(addable)
|
||||
return setmetatable({
|
||||
addable = addable,
|
||||
_all = vim.tbl_map(M.duplicate_expandable, addable:retrieve_all()),
|
||||
}, DupAddable_mt)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,439 @@
|
||||
local DynamicNode = require("luasnip.nodes.node").Node:new()
|
||||
local util = require("luasnip.util.util")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local Node = require("luasnip.nodes.node").Node
|
||||
local types = require("luasnip.util.types")
|
||||
local events = require("luasnip.util.events")
|
||||
local FunctionNode = require("luasnip.nodes.functionNode").FunctionNode
|
||||
local SnippetNode = require("luasnip.nodes.snippet").SN
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
|
||||
local function D(pos, fn, args, opts)
|
||||
opts = opts or {}
|
||||
|
||||
return DynamicNode:new({
|
||||
pos = pos,
|
||||
fn = fn,
|
||||
args = node_util.wrap_args(args),
|
||||
type = types.dynamicNode,
|
||||
mark = nil,
|
||||
user_args = opts.user_args or {},
|
||||
dependents = {},
|
||||
active = false,
|
||||
}, opts)
|
||||
end
|
||||
extend_decorator.register(D, { arg_indx = 4 })
|
||||
|
||||
function DynamicNode:input_enter(_, dry_run)
|
||||
if dry_run then
|
||||
dry_run.active[self] = true
|
||||
return
|
||||
end
|
||||
|
||||
self.visited = true
|
||||
self.active = true
|
||||
self.mark:update_opts(self.ext_opts.active)
|
||||
|
||||
self:event(events.enter)
|
||||
end
|
||||
|
||||
function DynamicNode:input_leave(_, dry_run)
|
||||
if dry_run then
|
||||
dry_run.active[self] = false
|
||||
return
|
||||
end
|
||||
self:event(events.leave)
|
||||
|
||||
self:update_dependents()
|
||||
self.active = false
|
||||
self.mark:update_opts(self:get_passive_ext_opts())
|
||||
end
|
||||
|
||||
function DynamicNode:get_static_text()
|
||||
if self.static_snip then
|
||||
return self.static_snip:get_static_text()
|
||||
else
|
||||
self:update_static()
|
||||
if self.static_snip then
|
||||
return self.static_snip:get_static_text()
|
||||
else
|
||||
return { "" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DynamicNode:get_docstring()
|
||||
if not self.docstring then
|
||||
if self.static_snip then
|
||||
self.docstring = self.static_snip:get_docstring()
|
||||
else
|
||||
self.docstring = { "" }
|
||||
end
|
||||
end
|
||||
return self.docstring
|
||||
end
|
||||
|
||||
-- DynamicNode's don't have static text, only set as visible.
|
||||
function DynamicNode:put_initial(_)
|
||||
self.visible = true
|
||||
end
|
||||
|
||||
function DynamicNode:indent(_) end
|
||||
|
||||
function DynamicNode:expand_tabs(_) end
|
||||
|
||||
function DynamicNode:jump_into(dir, no_move, dry_run)
|
||||
-- init dry_run-state for this node.
|
||||
self:init_dry_run_active(dry_run)
|
||||
|
||||
if self:is_active(dry_run) then
|
||||
self:input_leave(no_move, dry_run)
|
||||
|
||||
if dir == 1 then
|
||||
return self.next:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
return self.prev:jump_into(dir, no_move, dry_run)
|
||||
end
|
||||
else
|
||||
self:input_enter(no_move, dry_run)
|
||||
|
||||
if self.snip then
|
||||
return self.snip:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
-- this will immediately enter and leave, but IMO that's expected
|
||||
-- behaviour.
|
||||
self:input_leave(no_move, dry_run)
|
||||
if dir == 1 then
|
||||
return self.next:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
return self.prev:jump_into(dir, no_move, dry_run)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DynamicNode:update()
|
||||
local args = self:get_args()
|
||||
if vim.deep_equal(self.last_args, args) then
|
||||
-- no update, the args still match.
|
||||
return
|
||||
end
|
||||
|
||||
if not self.parent.snippet:extmarks_valid() then
|
||||
error("Refusing to update inside a snippet with invalid extmarks")
|
||||
end
|
||||
|
||||
local tmp
|
||||
if self.snip then
|
||||
if not args then
|
||||
-- a snippet exists, don't delete it.
|
||||
return
|
||||
end
|
||||
|
||||
-- build new snippet before exiting, markers may be needed for construncting.
|
||||
tmp = self.fn(
|
||||
args,
|
||||
self.parent,
|
||||
self.snip.old_state,
|
||||
unpack(self.user_args)
|
||||
)
|
||||
self.snip:exit()
|
||||
self.snip = nil
|
||||
|
||||
-- focuses node.
|
||||
self:set_text({ "" })
|
||||
else
|
||||
self:focus()
|
||||
if not args then
|
||||
-- no snippet exists, set an empty one.
|
||||
tmp = SnippetNode(nil, {})
|
||||
else
|
||||
-- also enter node here.
|
||||
tmp = self.fn(args, self.parent, nil, unpack(self.user_args))
|
||||
end
|
||||
end
|
||||
self.last_args = args
|
||||
|
||||
-- act as if snip is directly inside parent.
|
||||
tmp.parent = self.parent
|
||||
tmp.indx = self.indx
|
||||
|
||||
tmp.next = self
|
||||
tmp.prev = self
|
||||
|
||||
tmp.snippet = self.parent.snippet
|
||||
|
||||
tmp:resolve_child_ext_opts()
|
||||
tmp:resolve_node_ext_opts()
|
||||
tmp:subsnip_init()
|
||||
|
||||
tmp.mark =
|
||||
self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts()))
|
||||
tmp.dynamicNode = self
|
||||
tmp.update_dependents = function(node)
|
||||
node:_update_dependents()
|
||||
node.dynamicNode:update_dependents()
|
||||
end
|
||||
|
||||
tmp:init_positions(self.snip_absolute_position)
|
||||
tmp:init_insert_positions(self.snip_absolute_insert_position)
|
||||
|
||||
tmp:make_args_absolute()
|
||||
|
||||
tmp:set_dependents()
|
||||
tmp:set_argnodes(self.parent.snippet.dependents_dict)
|
||||
|
||||
if vim.bo.expandtab then
|
||||
tmp:expand_tabs(util.tab_width(), #self.parent.indentstr)
|
||||
end
|
||||
tmp:indent(self.parent.indentstr)
|
||||
|
||||
-- sets own extmarks false,true
|
||||
self:focus()
|
||||
local from, to = self.mark:pos_begin_end_raw()
|
||||
-- inserts nodes with extmarks false,false
|
||||
tmp:put_initial(from)
|
||||
-- adjust gravity in left side of snippet, such that it matches the current
|
||||
-- gravity of self.
|
||||
tmp:subtree_set_pos_rgrav(to, -1, true)
|
||||
|
||||
self.snip = tmp
|
||||
|
||||
-- Update, tbh no idea how that could come in handy, but should be done.
|
||||
-- Both are needed, because
|
||||
-- - a node could only depend on nodes outside of tmp
|
||||
-- - a node outside of tmp could depend on one inside of tmp
|
||||
tmp:update()
|
||||
tmp:update_all_dependents()
|
||||
|
||||
self:update_dependents()
|
||||
end
|
||||
|
||||
local update_errorstring = [[
|
||||
Error while evaluating dynamicNode@%d for snippet '%s':
|
||||
%s
|
||||
|
||||
:h luasnip-docstring for more info]]
|
||||
function DynamicNode:update_static()
|
||||
local args = self:get_static_args()
|
||||
if vim.deep_equal(self.last_static_args, args) then
|
||||
-- no update, the args still match.
|
||||
return
|
||||
end
|
||||
|
||||
local tmp, ok
|
||||
if self.static_snip then
|
||||
if not args then
|
||||
-- a snippet exists, don't delete it.
|
||||
return
|
||||
end
|
||||
|
||||
-- build new snippet before exiting, markers may be needed for construncting.
|
||||
ok, tmp = pcall(
|
||||
self.fn,
|
||||
args,
|
||||
self.parent,
|
||||
self.snip.old_state,
|
||||
unpack(self.user_args)
|
||||
)
|
||||
else
|
||||
if not args then
|
||||
-- no snippet exists, set an empty one.
|
||||
tmp = SnippetNode(nil, {})
|
||||
else
|
||||
-- also enter node here.
|
||||
ok, tmp =
|
||||
pcall(self.fn, args, self.parent, nil, unpack(self.user_args))
|
||||
end
|
||||
end
|
||||
if not ok then
|
||||
print(
|
||||
update_errorstring:format(self.indx, self.parent.snippet.name, tmp)
|
||||
)
|
||||
-- set empty snippet on failure
|
||||
tmp = SnippetNode(nil, {})
|
||||
end
|
||||
self.last_static_args = args
|
||||
|
||||
-- act as if snip is directly inside parent.
|
||||
tmp.parent = self.parent
|
||||
tmp.indx = self.indx
|
||||
tmp.pos = rawget(self, "pos")
|
||||
|
||||
tmp.next = self
|
||||
tmp.prev = self
|
||||
|
||||
-- doesn't matter here, but they'll have to be set.
|
||||
tmp.ext_opts = self.parent.ext_opts
|
||||
tmp.snippet = self.parent.snippet
|
||||
|
||||
tmp.dynamicNode = self
|
||||
tmp.update_dependents_static = function(node)
|
||||
node:_update_dependents_static()
|
||||
node.dynamicNode:update_dependents_static()
|
||||
end
|
||||
|
||||
tmp:resolve_child_ext_opts()
|
||||
tmp:resolve_node_ext_opts()
|
||||
tmp:subsnip_init()
|
||||
|
||||
tmp:init_positions(self.snip_absolute_position)
|
||||
tmp:init_insert_positions(self.snip_absolute_insert_position)
|
||||
|
||||
tmp:make_args_absolute()
|
||||
|
||||
tmp:set_dependents()
|
||||
tmp:set_argnodes(self.parent.snippet.dependents_dict)
|
||||
|
||||
-- do not expand tabs!! This is only necessary if the snippet is inserted
|
||||
-- in a buffer, some information is lost if tabs (indent) is replaced with
|
||||
-- whitespace.
|
||||
-- This might make a difference when another f/dynamicNode depends on this
|
||||
-- one, and the function expects expanded tabs... imo the function should
|
||||
-- be adjusted to accept any whitespace.
|
||||
tmp:indent(self.parent.indentstr)
|
||||
|
||||
tmp:static_init()
|
||||
|
||||
tmp:update_static()
|
||||
-- updates dependents in tmp.
|
||||
tmp:update_all_dependents_static()
|
||||
|
||||
self.static_snip = tmp
|
||||
-- updates own dependents.
|
||||
self:update_dependents_static()
|
||||
end
|
||||
|
||||
function DynamicNode:exit()
|
||||
self.visible = false
|
||||
self.mark:clear()
|
||||
-- check if snip actually exists, may not be the case if
|
||||
-- the surrounding snippet was deleted just before.
|
||||
if self.snip then
|
||||
self.snip:exit()
|
||||
end
|
||||
self.stored_snip = self.snip
|
||||
self.snip = nil
|
||||
self.active = false
|
||||
end
|
||||
|
||||
function DynamicNode:set_ext_opts(name)
|
||||
Node.set_ext_opts(self, name)
|
||||
|
||||
-- might not have been generated (missing nodes).
|
||||
if self.snip then
|
||||
self.snip:set_ext_opts(name)
|
||||
end
|
||||
end
|
||||
|
||||
function DynamicNode:store()
|
||||
if self.snip then
|
||||
self.snip:store()
|
||||
end
|
||||
end
|
||||
|
||||
function DynamicNode:update_restore()
|
||||
-- only restore snippet if arg-values still match.
|
||||
if self.stored_snip and vim.deep_equal(self:get_args(), self.last_args) then
|
||||
local tmp = self.stored_snip
|
||||
|
||||
tmp.mark =
|
||||
self.mark:copy_pos_gravs(vim.deepcopy(tmp:get_passive_ext_opts()))
|
||||
|
||||
-- position might (will probably!!) still have changed, so update it
|
||||
-- here too (as opposed to only in update).
|
||||
tmp:init_positions(self.snip_absolute_position)
|
||||
tmp:init_insert_positions(self.snip_absolute_insert_position)
|
||||
|
||||
tmp:make_args_absolute()
|
||||
|
||||
tmp:set_dependents()
|
||||
tmp:set_argnodes(self.parent.snippet.dependents_dict)
|
||||
|
||||
-- sets own extmarks false,true
|
||||
self:focus()
|
||||
-- inserts nodes with extmarks false,false
|
||||
local from, to = self.mark:pos_begin_end_raw()
|
||||
tmp:put_initial(from)
|
||||
-- adjust gravity in left side of snippet, such that it matches the current
|
||||
-- gravity of self.
|
||||
tmp:subtree_set_pos_rgrav(to, -1, true)
|
||||
|
||||
-- set snip before update_restore, since update_restore involves
|
||||
-- calling `focus`, and that needs `snip` to be set.
|
||||
-- If it is not set, tmp is not reachable via get_nodes_between.
|
||||
-- (TODO: This is pretty bad, have to rethink design sometime).
|
||||
self.snip = tmp
|
||||
|
||||
tmp:update_restore()
|
||||
else
|
||||
self:update()
|
||||
end
|
||||
end
|
||||
|
||||
function DynamicNode:find_node(predicate)
|
||||
if self.snip then
|
||||
if predicate(self.snip) then
|
||||
return self.snip
|
||||
else
|
||||
return self.snip:find_node(predicate)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function DynamicNode:insert_to_node_absolute(position)
|
||||
if #position == 0 then
|
||||
return self.absolute_position
|
||||
end
|
||||
return self.snip and self.snip:insert_to_node_absolute(position)
|
||||
end
|
||||
|
||||
function DynamicNode:init_insert_positions(position_so_far)
|
||||
Node.init_insert_positions(self, position_so_far)
|
||||
self.snip_absolute_insert_position =
|
||||
vim.deepcopy(self.absolute_insert_position)
|
||||
-- nodes of current snippet should have a 0 before.
|
||||
self.snip_absolute_insert_position[#self.snip_absolute_insert_position + 1] =
|
||||
0
|
||||
end
|
||||
|
||||
function DynamicNode:init_positions(position_so_far)
|
||||
Node.init_positions(self, position_so_far)
|
||||
self.snip_absolute_position = vim.deepcopy(self.absolute_position)
|
||||
-- Reach current snippet as snip_absolute_position..0.
|
||||
self.snip_absolute_position[#self.snip_absolute_position + 1] = 0
|
||||
end
|
||||
|
||||
DynamicNode.make_args_absolute = FunctionNode.make_args_absolute
|
||||
DynamicNode.set_dependents = FunctionNode.set_dependents
|
||||
|
||||
function DynamicNode:resolve_position(position)
|
||||
-- position must be 0, there are no other options.
|
||||
return self.snip
|
||||
end
|
||||
|
||||
function DynamicNode:subtree_set_pos_rgrav(pos, direction, rgrav)
|
||||
self.mark:set_rgrav(-direction, rgrav)
|
||||
if self.snip then
|
||||
self.snip:subtree_set_pos_rgrav(pos, direction, rgrav)
|
||||
end
|
||||
end
|
||||
|
||||
function DynamicNode:subtree_set_rgrav(rgrav)
|
||||
self.mark:set_rgravs(rgrav, rgrav)
|
||||
if self.snip then
|
||||
self.snip:subtree_set_rgrav(rgrav)
|
||||
end
|
||||
end
|
||||
|
||||
function DynamicNode:extmarks_valid()
|
||||
if self.snip then
|
||||
return node_util.generic_extmarks_valid(self, self.snip)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return {
|
||||
D = D,
|
||||
}
|
||||
@ -0,0 +1,155 @@
|
||||
local Node = require("luasnip.nodes.node").Node
|
||||
local FunctionNode = Node:new()
|
||||
local util = require("luasnip.util.util")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local types = require("luasnip.util.types")
|
||||
local tNode = require("luasnip.nodes.textNode").textNode
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
local key_indexer = require("luasnip.nodes.key_indexer")
|
||||
|
||||
local function F(fn, args, opts)
|
||||
opts = opts or {}
|
||||
|
||||
return FunctionNode:new({
|
||||
fn = fn,
|
||||
args = node_util.wrap_args(args),
|
||||
type = types.functionNode,
|
||||
mark = nil,
|
||||
user_args = opts.user_args or {},
|
||||
}, opts)
|
||||
end
|
||||
extend_decorator.register(F, { arg_indx = 3 })
|
||||
|
||||
FunctionNode.input_enter = tNode.input_enter
|
||||
|
||||
function FunctionNode:get_static_text()
|
||||
-- static_text will already have been generated, if possible.
|
||||
-- If it isn't generated, prevent errors by just setting it to empty text.
|
||||
if not self.static_text then
|
||||
self.static_text = { "" }
|
||||
end
|
||||
return self.static_text
|
||||
end
|
||||
|
||||
-- function-text will not stand out in any way in docstring.
|
||||
FunctionNode.get_docstring = FunctionNode.get_static_text
|
||||
|
||||
function FunctionNode:update()
|
||||
local args = self:get_args()
|
||||
-- skip this update if
|
||||
-- - not all nodes are available.
|
||||
-- - the args haven't changed.
|
||||
if not args or vim.deep_equal(args, self.last_args) then
|
||||
return
|
||||
end
|
||||
|
||||
if not self.parent.snippet:extmarks_valid() then
|
||||
error("Refusing to update inside a snippet with invalid extmarks")
|
||||
end
|
||||
|
||||
self.last_args = args
|
||||
local text =
|
||||
util.to_string_table(self.fn(args, self.parent, unpack(self.user_args)))
|
||||
if vim.bo.expandtab then
|
||||
util.expand_tabs(text, util.tab_width(), #self.parent.indentstr)
|
||||
end
|
||||
|
||||
-- don't expand tabs in parent.indentstr, use it as-is.
|
||||
self:set_text(util.indent(text, self.parent.indentstr))
|
||||
self:update_dependents()
|
||||
end
|
||||
|
||||
local update_errorstring = [[
|
||||
Error while evaluating functionNode@%d for snippet '%s':
|
||||
%s
|
||||
|
||||
:h luasnip-docstring for more info]]
|
||||
function FunctionNode:update_static()
|
||||
local args = self:get_static_args()
|
||||
-- skip this update if
|
||||
-- - not all nodes are available.
|
||||
-- - the args haven't changed.
|
||||
if not args or vim.deep_equal(args, self.last_args) then
|
||||
return
|
||||
end
|
||||
-- should be okay to set last_args even if `fn` potentially fails, future
|
||||
-- updates will fail aswell, if not the `fn` also doesn't always work
|
||||
-- correctly in normal expansion.
|
||||
self.last_args = args
|
||||
local ok, static_text =
|
||||
pcall(self.fn, args, self.parent, unpack(self.user_args))
|
||||
if not ok then
|
||||
print(
|
||||
update_errorstring:format(
|
||||
self.indx,
|
||||
self.parent.snippet.name,
|
||||
static_text
|
||||
)
|
||||
)
|
||||
static_text = { "" }
|
||||
end
|
||||
self.static_text =
|
||||
util.indent(util.to_string_table(static_text), self.parent.indentstr)
|
||||
end
|
||||
|
||||
function FunctionNode:update_restore()
|
||||
-- only if args still match.
|
||||
if self.static_text and vim.deep_equal(self:get_args(), self.last_args) then
|
||||
self:set_text(self.static_text)
|
||||
else
|
||||
self:update()
|
||||
end
|
||||
end
|
||||
|
||||
-- FunctionNode's don't have static text, only set visibility.
|
||||
function FunctionNode:put_initial(_)
|
||||
self.visible = true
|
||||
end
|
||||
|
||||
function FunctionNode:indent(_) end
|
||||
|
||||
function FunctionNode:expand_tabs(_) end
|
||||
|
||||
function FunctionNode:make_args_absolute(position_so_far)
|
||||
self.args_absolute = {}
|
||||
node_util.make_args_absolute(self.args, position_so_far, self.args_absolute)
|
||||
end
|
||||
|
||||
function FunctionNode:set_dependents()
|
||||
local dict = self.parent.snippet.dependents_dict
|
||||
local append_list =
|
||||
vim.list_extend({ "dependents" }, self.absolute_position)
|
||||
append_list[#append_list + 1] = "dependent"
|
||||
|
||||
for _, arg in ipairs(self.args_absolute) do
|
||||
-- if arg is a luasnip-node, just insert it as the key.
|
||||
-- important!! rawget, because indexing absolute_indexer with some key
|
||||
-- appends the key.
|
||||
-- Maybe this is stupid??
|
||||
if rawget(arg, "type") ~= nil then
|
||||
dict:set(vim.list_extend({ arg }, append_list), self)
|
||||
elseif arg.absolute_insert_position then
|
||||
-- copy absolute_insert_position, list_extend mutates.
|
||||
dict:set(
|
||||
vim.list_extend(
|
||||
vim.deepcopy(arg.absolute_insert_position),
|
||||
append_list
|
||||
),
|
||||
self
|
||||
)
|
||||
elseif key_indexer.is_key(arg) then
|
||||
dict:set(vim.list_extend({ "key", arg.key }, append_list), self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function FunctionNode:is_interactive()
|
||||
-- the function node is only evaluated once if it has no argnodes -> it's
|
||||
-- not interactive then.
|
||||
return #self.args ~= 0
|
||||
end
|
||||
|
||||
return {
|
||||
F = F,
|
||||
FunctionNode = FunctionNode,
|
||||
}
|
||||
@ -0,0 +1,330 @@
|
||||
local Node = require("luasnip.nodes.node")
|
||||
local InsertNode = Node.Node:new()
|
||||
local ExitNode = InsertNode:new()
|
||||
local util = require("luasnip.util.util")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local types = require("luasnip.util.types")
|
||||
local events = require("luasnip.util.events")
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
|
||||
local function I(pos, static_text, opts)
|
||||
static_text = util.to_string_table(static_text)
|
||||
|
||||
if pos == 0 then
|
||||
return ExitNode:new({
|
||||
pos = pos,
|
||||
static_text = static_text,
|
||||
mark = nil,
|
||||
dependents = {},
|
||||
type = types.exitNode,
|
||||
-- will only be needed for 0-node, -1-node isn't set with this.
|
||||
ext_gravities_active = { false, false },
|
||||
}, opts)
|
||||
else
|
||||
return InsertNode:new({
|
||||
pos = pos,
|
||||
static_text = static_text,
|
||||
mark = nil,
|
||||
dependents = {},
|
||||
type = types.insertNode,
|
||||
inner_active = false,
|
||||
}, opts)
|
||||
end
|
||||
end
|
||||
extend_decorator.register(I, { arg_indx = 3 })
|
||||
|
||||
function ExitNode:input_enter(no_move, dry_run)
|
||||
if dry_run then
|
||||
return
|
||||
end
|
||||
|
||||
-- Don't enter node for -1-node, it isn't in the node-table.
|
||||
if self.pos == 0 then
|
||||
InsertNode.input_enter(self, no_move, dry_run)
|
||||
else
|
||||
-- -1-node:
|
||||
-- set rgrav true on left side of snippet. Text inserted now pushes the
|
||||
-- snippet, and is not contained in it.
|
||||
local begin_pos = self.mark:pos_begin_raw()
|
||||
self.parent:subtree_set_pos_rgrav(begin_pos, 1, true)
|
||||
|
||||
if not no_move then
|
||||
if vim.fn.mode() == "i" then
|
||||
util.insert_move_on(begin_pos)
|
||||
else
|
||||
vim.api.nvim_feedkeys(
|
||||
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
|
||||
"n",
|
||||
true
|
||||
)
|
||||
util.normal_move_on_insert(begin_pos)
|
||||
end
|
||||
end
|
||||
|
||||
self:event(events.enter)
|
||||
end
|
||||
end
|
||||
|
||||
function ExitNode:focus()
|
||||
local lrgrav, rrgrav
|
||||
local snippet = self.parent
|
||||
-- if last of first node of the snippet, make inserted text move out of snippet.
|
||||
if snippet.nodes[#snippet.nodes] == self then
|
||||
lrgrav = false
|
||||
rrgrav = false
|
||||
elseif snippet.nodes[1] == self then
|
||||
lrgrav = true
|
||||
rrgrav = true
|
||||
else
|
||||
lrgrav = false
|
||||
rrgrav = true
|
||||
end
|
||||
|
||||
Node.focus_node(self, lrgrav, rrgrav)
|
||||
end
|
||||
|
||||
function ExitNode:input_leave(no_move, dry_run)
|
||||
if dry_run then
|
||||
return
|
||||
end
|
||||
|
||||
if self.pos == 0 then
|
||||
InsertNode.input_leave(self, no_move, dry_run)
|
||||
else
|
||||
self:event(events.leave)
|
||||
end
|
||||
end
|
||||
|
||||
function ExitNode:_update_dependents() end
|
||||
function ExitNode:update_dependents() end
|
||||
function ExitNode:update_all_dependents() end
|
||||
|
||||
function ExitNode:_update_dependents_static() end
|
||||
function ExitNode:update_dependents_static() end
|
||||
function ExitNode:update_all_dependents_static() end
|
||||
function ExitNode:is_interactive()
|
||||
return true
|
||||
end
|
||||
|
||||
function InsertNode:input_enter(no_move, dry_run)
|
||||
if dry_run then
|
||||
return
|
||||
end
|
||||
|
||||
self.visited = true
|
||||
self.mark:update_opts(self.ext_opts.active)
|
||||
|
||||
-- no_move only prevents moving the cursor, but the active node should
|
||||
-- still be focused.
|
||||
self:focus()
|
||||
|
||||
if not no_move then
|
||||
-- SELECT snippet text only when there is text to select (more oft than not there isnt).
|
||||
local mark_begin_pos, mark_end_pos = self.mark:pos_begin_end_raw()
|
||||
if not util.pos_equal(mark_begin_pos, mark_end_pos) then
|
||||
util.any_select(mark_begin_pos, mark_end_pos)
|
||||
else
|
||||
-- if current and target mode is INSERT, there's no reason to leave it.
|
||||
if vim.fn.mode() == "i" then
|
||||
util.insert_move_on(mark_begin_pos)
|
||||
else
|
||||
-- mode might be VISUAL or something else, but <Esc> always leads to normal.
|
||||
vim.api.nvim_feedkeys(
|
||||
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
|
||||
"n",
|
||||
true
|
||||
)
|
||||
util.normal_move_on_insert(mark_begin_pos)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self:event(events.enter)
|
||||
end
|
||||
|
||||
-- only necessary for insertNodes, inner_active (unlike `active`) does not occur
|
||||
-- in other nodes.
|
||||
-- Since insertNodes don't have `active`, we can use the dry_run.active-field
|
||||
-- for this.
|
||||
function InsertNode:init_dry_run_inner_active(dry_run)
|
||||
if dry_run and dry_run.active[self] == nil then
|
||||
dry_run.active[self] = self.inner_active
|
||||
end
|
||||
end
|
||||
function InsertNode:is_inner_active(dry_run)
|
||||
return (not dry_run and self.inner_active)
|
||||
or (dry_run and dry_run.active[self])
|
||||
end
|
||||
|
||||
function InsertNode:jump_into(dir, no_move, dry_run)
|
||||
self:init_dry_run_inner_active(dry_run)
|
||||
|
||||
if self:is_inner_active(dry_run) then
|
||||
if dir == 1 then
|
||||
if self.next then
|
||||
self:input_leave_children(dry_run)
|
||||
self:input_leave(no_move, dry_run)
|
||||
return self.next:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
else
|
||||
if self.prev then
|
||||
self:input_leave_children(dry_run)
|
||||
self:input_leave(no_move, dry_run)
|
||||
return self.prev:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
else
|
||||
self:input_enter(no_move, dry_run)
|
||||
return self
|
||||
end
|
||||
end
|
||||
|
||||
function ExitNode:jump_from(dir, no_move, dry_run)
|
||||
self:init_dry_run_inner_active(dry_run)
|
||||
|
||||
local next_node = util.ternary(dir == 1, self.next, self.prev)
|
||||
local next_inner_node =
|
||||
util.ternary(dir == 1, self.inner_first, self.inner_last)
|
||||
|
||||
if next_inner_node then
|
||||
self:input_enter_children(dry_run)
|
||||
return next_inner_node:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
if next_node then
|
||||
local next_node_dry_run = { active = {} }
|
||||
-- don't have to `init_dry_run_inner_active` since this node does
|
||||
-- not have children active if jump_from is called.
|
||||
|
||||
-- true: don't move
|
||||
local target_node =
|
||||
next_node:jump_into(dir, true, next_node_dry_run)
|
||||
-- if there is no node that can serve as jump-target, just remain
|
||||
-- here.
|
||||
-- Regular insertNodes don't have to handle this, since there is
|
||||
-- always an exitNode or another insertNode at their endpoints.
|
||||
if not target_node then
|
||||
return self
|
||||
end
|
||||
|
||||
self:input_leave(no_move, dry_run)
|
||||
return next_node:jump_into(dir, no_move, dry_run) or self
|
||||
else
|
||||
return self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function InsertNode:jump_from(dir, no_move, dry_run)
|
||||
self:init_dry_run_inner_active(dry_run)
|
||||
|
||||
local next_node = util.ternary(dir == 1, self.next, self.prev)
|
||||
local next_inner_node =
|
||||
util.ternary(dir == 1, self.inner_first, self.inner_last)
|
||||
|
||||
if next_inner_node then
|
||||
self:input_enter_children(dry_run)
|
||||
return next_inner_node:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
if next_node then
|
||||
self:input_leave(no_move, dry_run)
|
||||
return next_node:jump_into(dir, no_move, dry_run)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function InsertNode:input_enter_children(dry_run)
|
||||
if dry_run then
|
||||
dry_run.active[self] = true
|
||||
else
|
||||
self.inner_active = true
|
||||
end
|
||||
end
|
||||
function InsertNode:input_leave_children(dry_run)
|
||||
if dry_run then
|
||||
dry_run.active[self] = false
|
||||
else
|
||||
self.inner_active = false
|
||||
end
|
||||
end
|
||||
|
||||
function InsertNode:input_leave(_, dry_run)
|
||||
if dry_run then
|
||||
return
|
||||
end
|
||||
|
||||
self:event(events.leave)
|
||||
|
||||
self:update_dependents()
|
||||
self.mark:update_opts(self:get_passive_ext_opts())
|
||||
end
|
||||
|
||||
function InsertNode:exit()
|
||||
if self.inner_first then
|
||||
self.inner_first:exit()
|
||||
end
|
||||
self.visible = false
|
||||
self.inner_first = nil
|
||||
self.inner_last = nil
|
||||
self.inner_active = false
|
||||
self.mark:clear()
|
||||
end
|
||||
|
||||
function InsertNode:get_docstring()
|
||||
-- copy as to not in-place-modify static text.
|
||||
return util.string_wrap(self.static_text, rawget(self, "pos"))
|
||||
end
|
||||
|
||||
function InsertNode:is_interactive()
|
||||
return true
|
||||
end
|
||||
|
||||
function InsertNode:child_snippets()
|
||||
local own_child_snippets = {}
|
||||
for _, child_snippet in ipairs(self.parent.snippet.child_snippets) do
|
||||
if child_snippet.parent_node == self then
|
||||
table.insert(own_child_snippets, child_snippet)
|
||||
end
|
||||
end
|
||||
return own_child_snippets
|
||||
end
|
||||
|
||||
function InsertNode:subtree_set_pos_rgrav(pos, direction, rgrav)
|
||||
self.mark:set_rgrav(-direction, rgrav)
|
||||
|
||||
local own_child_snippets = self:child_snippets()
|
||||
|
||||
local child_from_indx
|
||||
if direction == 1 then
|
||||
child_from_indx = 1
|
||||
else
|
||||
child_from_indx = #own_child_snippets
|
||||
end
|
||||
|
||||
node_util.nodelist_adjust_rgravs(
|
||||
own_child_snippets,
|
||||
child_from_indx,
|
||||
pos,
|
||||
direction,
|
||||
rgrav,
|
||||
-- don't assume that the child-snippets are all adjacent.
|
||||
false
|
||||
)
|
||||
end
|
||||
|
||||
function InsertNode:subtree_set_rgrav(rgrav)
|
||||
self.mark:set_rgravs(rgrav, rgrav)
|
||||
|
||||
local own_child_snippets = self:child_snippets()
|
||||
|
||||
for _, child_snippet in ipairs(own_child_snippets) do
|
||||
child_snippet:subtree_set_rgrav(rgrav)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
I = I,
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
local M = {}
|
||||
|
||||
local key_mt = {}
|
||||
function M.new_key(key)
|
||||
return setmetatable({ key = key }, key_mt)
|
||||
end
|
||||
|
||||
function M.is_key(t)
|
||||
return getmetatable(t) == key_mt
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,117 @@
|
||||
local snip_mod = require("luasnip.nodes.snippet")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
|
||||
local VirtualSnippet = {}
|
||||
local VirtualSnippet_mt = { __index = VirtualSnippet }
|
||||
|
||||
function VirtualSnippet:get_docstring()
|
||||
return self.snippet:get_docstring()
|
||||
end
|
||||
function VirtualSnippet:copy()
|
||||
local copy = self.snippet:copy()
|
||||
copy.id = self.id
|
||||
|
||||
return copy
|
||||
end
|
||||
|
||||
-- VirtualSnippet has all the fields for executing these methods.
|
||||
VirtualSnippet.matches = snip_mod.Snippet.matches
|
||||
VirtualSnippet.invalidate = snip_mod.Snippet.invalidate
|
||||
|
||||
---Create new virtual snippet, ie. an object which is capable of performning
|
||||
---all the functions expected from a snippet which is yet to be expanded
|
||||
---(`matches`,`get_docstring`,`invalidate`,`retrieve_all`,`copy`)
|
||||
---@param context context as defined for snippet-constructor. Table, not nil.
|
||||
---@param snippet The snippet this virtual snippet will return on `copy`, also not nil.
|
||||
---@param opts opts as defined for snippet-constructor. Has to be a table, may be empty.
|
||||
local function new_virtual_snippet(context, snippet, opts)
|
||||
-- init fields necessary for matches, invalidate, adding the snippet.
|
||||
local o = snip_mod.init_snippet_context(context, opts)
|
||||
o.snippet = snippet
|
||||
|
||||
setmetatable(o, VirtualSnippet_mt)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
local MultiSnippet = {}
|
||||
local MultiSnippet_mt = { __index = MultiSnippet }
|
||||
|
||||
function MultiSnippet:retrieve_all()
|
||||
return self.v_snips
|
||||
end
|
||||
|
||||
local function multisnippet_from_snippet_obj(contexts, snippet, snippet_opts)
|
||||
assert(
|
||||
type(contexts) == "table",
|
||||
"multisnippet: expected contexts to be a table."
|
||||
)
|
||||
local common_context = node_util.wrap_context(contexts.common) or {}
|
||||
|
||||
local v_snips = {}
|
||||
for _, context in ipairs(contexts) do
|
||||
local complete_context = vim.tbl_extend(
|
||||
"keep",
|
||||
node_util.wrap_context(context),
|
||||
common_context
|
||||
)
|
||||
table.insert(
|
||||
v_snips,
|
||||
new_virtual_snippet(complete_context, snippet, snippet_opts)
|
||||
)
|
||||
end
|
||||
|
||||
local o = {
|
||||
v_snips = v_snips,
|
||||
}
|
||||
|
||||
setmetatable(o, MultiSnippet_mt)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
local function multisnippet_from_nodes(contexts, nodes, opts)
|
||||
opts = opts or {}
|
||||
local common_snip_opts = opts.common_opts or {}
|
||||
|
||||
-- create snippet without `context`-fields!
|
||||
-- compare to `S` (aka `s`, the default snippet-constructor) in
|
||||
-- `nodes/snippet.lua`.
|
||||
return multisnippet_from_snippet_obj(
|
||||
contexts,
|
||||
snip_mod._S(
|
||||
snip_mod.init_snippet_opts(common_snip_opts),
|
||||
nodes,
|
||||
common_snip_opts
|
||||
),
|
||||
common_snip_opts
|
||||
)
|
||||
end
|
||||
|
||||
local function extend_multisnippet_contexts(passed_arg, extend_arg)
|
||||
-- extend passed arg with contexts passed in extend-call
|
||||
vim.list_extend(passed_arg, extend_arg)
|
||||
|
||||
-- extend ("keep") valid keyword-arguments.
|
||||
passed_arg.common = vim.tbl_deep_extend(
|
||||
"keep",
|
||||
node_util.wrap_context(passed_arg.common) or {},
|
||||
node_util.wrap_context(extend_arg.common) or {}
|
||||
)
|
||||
|
||||
return passed_arg
|
||||
end
|
||||
extend_decorator.register(
|
||||
multisnippet_from_nodes,
|
||||
-- first arg needs special handling (extend list of contexts (index i
|
||||
-- becomes i+#passed_arg, not i again))
|
||||
{ arg_indx = 1, extend = extend_multisnippet_contexts },
|
||||
-- opts can just be `vim.tbl_extend`ed.
|
||||
{ arg_indx = 3 }
|
||||
)
|
||||
|
||||
return {
|
||||
new_multisnippet = multisnippet_from_nodes,
|
||||
_raw_ms = multisnippet_from_snippet_obj,
|
||||
}
|
||||
@ -0,0 +1,643 @@
|
||||
local session = require("luasnip.session")
|
||||
local util = require("luasnip.util.util")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local ext_util = require("luasnip.util.ext_opts")
|
||||
local events = require("luasnip.util.events")
|
||||
local key_indexer = require("luasnip.nodes.key_indexer")
|
||||
local types = require("luasnip.util.types")
|
||||
|
||||
local Node = {}
|
||||
|
||||
function Node:new(o, opts)
|
||||
o = o or {}
|
||||
|
||||
-- visible is true if the node is visible on-screen, during normal
|
||||
-- expansion, static_visible is needed for eg. get_static_text, where
|
||||
-- argnodes in inactive choices will happily provide their static text,
|
||||
-- which leads to inaccurate docstrings.
|
||||
o.visible = false
|
||||
o.static_visible = false
|
||||
o.old_text = {}
|
||||
o.visited = false
|
||||
-- override existing keys, might be necessary due to double-init from
|
||||
-- snippetProxy, but shouldn't hurt.
|
||||
o = vim.tbl_extend("force", o, node_util.init_node_opts(opts or {}))
|
||||
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function Node:get_static_text()
|
||||
-- return nil if not visible.
|
||||
-- This will prevent updates if not all nodes are visible during
|
||||
-- docstring/static_text-generation. (One example that would otherwise fail
|
||||
-- is the following snippet:
|
||||
--
|
||||
-- s("trig", {
|
||||
-- i(1, "cccc"),
|
||||
-- t" ",
|
||||
-- c(2, {
|
||||
-- t"aaaa",
|
||||
-- i(nil, "bbbb")
|
||||
-- }),
|
||||
-- f(function(args) return args[1][1]..args[2][1] end, {ai[2][2], 1} )
|
||||
-- })
|
||||
--
|
||||
-- )
|
||||
-- By also allowing visible, and not only static_visible, the docstrings
|
||||
-- generated during `get_current_choices` (ie. without having the whole
|
||||
-- snippet `static_init`ed) get better.
|
||||
if not self.visible and not self.static_visible then
|
||||
return nil
|
||||
end
|
||||
return self.static_text
|
||||
end
|
||||
|
||||
function Node:get_docstring()
|
||||
-- visibility only matters for get_static_text because that's called for
|
||||
-- argnodes whereas get_docstring will only be called for actually
|
||||
-- visible nodes.
|
||||
return self.static_text
|
||||
end
|
||||
|
||||
function Node:put_initial(pos)
|
||||
-- access static text directly, get_static_text() won't work due to
|
||||
-- static_visible not being set.
|
||||
util.put(self.static_text, pos)
|
||||
self.visible = true
|
||||
end
|
||||
|
||||
function Node:input_enter(_, _)
|
||||
self.visited = true
|
||||
self.mark:update_opts(self.ext_opts.active)
|
||||
|
||||
self:event(events.enter)
|
||||
end
|
||||
|
||||
-- dry_run: if not nil, it has to be a table with the key `active` also a table.
|
||||
-- dry_run.active[node] stores whether the node is "active" in the dry run (we
|
||||
-- can't change the `active`-state in the actual node, so changes to the
|
||||
-- active-state are stored in the `dry_run`-table, which is passed to all nodes
|
||||
-- that participate in the jump)
|
||||
-- The changes to `active` have to be stored. Otherwise, `dry_run` can lead to
|
||||
-- endless loops in cases like:
|
||||
-- ```lua
|
||||
-- s({ trig = 'n' } , { i(1, "1"), sn(2, {t"asdf"}), i(3, "3") })
|
||||
-- ```
|
||||
--
|
||||
-- Here, jumping from 1 will first set active on the snippetNode, then, since
|
||||
-- there are no interactive nodes inside it, and since active is set, we will
|
||||
-- jump to the `i(3)`.
|
||||
-- If active is not set during the dry_run, we will just keep jumping into the
|
||||
-- inner textNode.
|
||||
--
|
||||
-- A similar problem occurs in nested expansions (insertNode.inner_active
|
||||
-- is not set).
|
||||
function Node:jump_into(_, no_move, dry_run)
|
||||
if not dry_run then
|
||||
self:input_enter(no_move, dry_run)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function Node:jump_from(dir, no_move, dry_run)
|
||||
self:input_leave(no_move, dry_run)
|
||||
if dir == 1 then
|
||||
if self.next then
|
||||
return self.next:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
else
|
||||
if self.prev then
|
||||
return self.prev:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Node:jumpable(dir)
|
||||
if dir == 1 then
|
||||
return self.next ~= nil
|
||||
else
|
||||
return self.prev ~= nil
|
||||
end
|
||||
end
|
||||
|
||||
function Node:get_text()
|
||||
if not self.visible then
|
||||
return nil
|
||||
end
|
||||
local ok, text = pcall(function()
|
||||
local from_pos, to_pos = self.mark:pos_begin_end_raw()
|
||||
|
||||
-- end-exclusive indexing.
|
||||
local lines =
|
||||
vim.api.nvim_buf_get_lines(0, from_pos[1], to_pos[1] + 1, false)
|
||||
|
||||
if #lines == 1 then
|
||||
lines[1] = string.sub(lines[1], from_pos[2] + 1, to_pos[2])
|
||||
else
|
||||
lines[1] = string.sub(lines[1], from_pos[2] + 1, #lines[1])
|
||||
|
||||
-- node-range is end-exclusive.
|
||||
lines[#lines] = string.sub(lines[#lines], 1, to_pos[2])
|
||||
end
|
||||
return lines
|
||||
end)
|
||||
-- if deleted.
|
||||
return ok and text or { "" }
|
||||
end
|
||||
|
||||
function Node:set_old_text()
|
||||
self.old_text = self:get_text()
|
||||
end
|
||||
|
||||
function Node:exit()
|
||||
self.visible = false
|
||||
self.mark:clear()
|
||||
end
|
||||
|
||||
function Node:get_passive_ext_opts()
|
||||
if self.visited then
|
||||
return self.ext_opts.visited
|
||||
else
|
||||
return self.ext_opts.unvisited
|
||||
end
|
||||
end
|
||||
|
||||
function Node:input_leave(_, dry_run)
|
||||
if dry_run then
|
||||
return
|
||||
end
|
||||
self:event(events.leave)
|
||||
|
||||
self.mark:update_opts(self:get_passive_ext_opts())
|
||||
end
|
||||
function Node:input_leave_children() end
|
||||
function Node:input_enter_children() end
|
||||
|
||||
local function find_dependents(self, position_self, dict)
|
||||
local nodes = {}
|
||||
|
||||
-- this might also be called from a node which does not possess a position!
|
||||
-- (for example, a functionNode may be depended upon via its key)
|
||||
if position_self then
|
||||
position_self[#position_self + 1] = "dependents"
|
||||
vim.list_extend(nodes, dict:find_all(position_self, "dependent") or {})
|
||||
position_self[#position_self] = nil
|
||||
end
|
||||
|
||||
vim.list_extend(
|
||||
nodes,
|
||||
dict:find_all({ self, "dependents" }, "dependent") or {}
|
||||
)
|
||||
|
||||
if self.key then
|
||||
vim.list_extend(
|
||||
nodes,
|
||||
dict:find_all({ "key", self.key, "dependents" }, "dependent") or {}
|
||||
)
|
||||
end
|
||||
|
||||
return nodes
|
||||
end
|
||||
|
||||
function Node:_update_dependents()
|
||||
local dependent_nodes = find_dependents(
|
||||
self,
|
||||
self.absolute_insert_position,
|
||||
self.parent.snippet.dependents_dict
|
||||
)
|
||||
if #dependent_nodes == 0 then
|
||||
return
|
||||
end
|
||||
for _, node in ipairs(dependent_nodes) do
|
||||
if node.visible then
|
||||
node:update()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- _update_dependents is the function to update the nodes' dependents,
|
||||
-- update_dependents is what will actually be called.
|
||||
-- This allows overriding update_dependents in a parent-node (eg. snippetNode)
|
||||
-- while still having access to the original function (for subsequent overrides).
|
||||
Node.update_dependents = Node._update_dependents
|
||||
-- update_all_dependents is used to update all nodes' dependents in a
|
||||
-- snippet-tree. Necessary in eg. set_choice (especially since nodes may have
|
||||
-- dependencies outside the tree itself, so update_all_dependents should take
|
||||
-- care of those too.)
|
||||
Node.update_all_dependents = Node._update_dependents
|
||||
|
||||
function Node:_update_dependents_static()
|
||||
local dependent_nodes = find_dependents(
|
||||
self,
|
||||
self.absolute_insert_position,
|
||||
self.parent.snippet.dependents_dict
|
||||
)
|
||||
if #dependent_nodes == 0 then
|
||||
return
|
||||
end
|
||||
for _, node in ipairs(dependent_nodes) do
|
||||
if node.static_visible then
|
||||
node:update_static()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Node.update_dependents_static = Node._update_dependents_static
|
||||
Node.update_all_dependents_static = Node._update_dependents_static
|
||||
|
||||
function Node:update() end
|
||||
|
||||
function Node:update_static() end
|
||||
|
||||
function Node:expand_tabs(tabwidth, indentstr)
|
||||
util.expand_tabs(self.static_text, tabwidth, indentstr)
|
||||
end
|
||||
|
||||
function Node:indent(indentstr)
|
||||
util.indent(self.static_text, indentstr)
|
||||
end
|
||||
|
||||
function Node:subsnip_init() end
|
||||
|
||||
function Node:init_positions(position_so_far)
|
||||
self.absolute_position = vim.deepcopy(position_so_far)
|
||||
end
|
||||
|
||||
function Node:init_insert_positions(position_so_far)
|
||||
self.absolute_insert_position = vim.deepcopy(position_so_far)
|
||||
end
|
||||
|
||||
function Node:event(event)
|
||||
local node_callback = self.node_callbacks[event]
|
||||
if node_callback then
|
||||
node_callback(self)
|
||||
end
|
||||
|
||||
-- try to get the callback from the parent.
|
||||
if self.pos then
|
||||
-- node needs position to get callback (nodes may not have position if
|
||||
-- defined in a choiceNode, ie. c(1, {
|
||||
-- i(nil, {"works!"})
|
||||
-- }))
|
||||
-- works just fine.
|
||||
local parent_callback = self.parent.callbacks[self.pos][event]
|
||||
if parent_callback then
|
||||
parent_callback(self)
|
||||
end
|
||||
end
|
||||
|
||||
session.event_node = self
|
||||
vim.api.nvim_exec_autocmds("User", {
|
||||
pattern = "Luasnip" .. events.to_string(self.type, event),
|
||||
modeline = false,
|
||||
})
|
||||
end
|
||||
|
||||
local function get_args(node, get_text_func_name)
|
||||
local argnodes_text = {}
|
||||
for _, arg in ipairs(node.args_absolute) do
|
||||
local argnode
|
||||
if key_indexer.is_key(arg) then
|
||||
argnode = node.parent.snippet.dependents_dict:get({
|
||||
"key",
|
||||
arg.key,
|
||||
"node",
|
||||
})
|
||||
else
|
||||
-- since arg may be a node, it may not be initialized in the snippet
|
||||
-- and therefore not have an absolute_insert_position. Check for that.
|
||||
if not arg.absolute_insert_position then
|
||||
-- the node is not (yet, maybe) visible.
|
||||
return nil
|
||||
end
|
||||
local dict_key = arg.absolute_insert_position
|
||||
-- will append to arg.absolute_insert_position, but it's restored
|
||||
-- two lines down.
|
||||
-- (dict:get shouldn't (yeah yeah, you never know, but this really
|
||||
-- shouldn't) fail, so we don't worry with pcall)
|
||||
table.insert(dict_key, "node")
|
||||
argnode = node.parent.snippet.dependents_dict:get(dict_key)
|
||||
dict_key[#dict_key] = nil
|
||||
end
|
||||
-- maybe the node is part of a dynamicNode and not yet generated.
|
||||
if not argnode then
|
||||
return nil
|
||||
end
|
||||
|
||||
local argnode_text = argnode[get_text_func_name](argnode)
|
||||
-- can only occur with `get_text`. If one returns nil, the argnode
|
||||
-- isn't visible or some other error occured. Either way, return nil
|
||||
-- to signify that not all argnodes are available.
|
||||
if not argnode_text then
|
||||
return nil
|
||||
end
|
||||
table.insert(argnodes_text, argnode_text)
|
||||
end
|
||||
|
||||
return argnodes_text
|
||||
end
|
||||
|
||||
function Node:get_args()
|
||||
return get_args(self, "get_text")
|
||||
end
|
||||
function Node:get_static_args()
|
||||
return get_args(self, "get_static_text")
|
||||
end
|
||||
|
||||
function Node:get_jump_index()
|
||||
return self.pos
|
||||
end
|
||||
|
||||
function Node:set_ext_opts(name)
|
||||
-- differentiate, either visited or unvisited needs to be set.
|
||||
if name == "passive" then
|
||||
self.mark:update_opts(self:get_passive_ext_opts())
|
||||
else
|
||||
self.mark:update_opts(self.ext_opts[name])
|
||||
end
|
||||
end
|
||||
|
||||
-- for insert,functionNode.
|
||||
function Node:store()
|
||||
self.static_text = self:get_text()
|
||||
end
|
||||
|
||||
function Node:update_restore() end
|
||||
|
||||
-- find_node only needs to check children, self is checked by the parent.
|
||||
function Node:find_node()
|
||||
return nil
|
||||
end
|
||||
|
||||
Node.ext_gravities_active = { false, true }
|
||||
|
||||
function Node:insert_to_node_absolute(position)
|
||||
-- this node is a leaf, just return its position
|
||||
return self.absolute_position
|
||||
end
|
||||
|
||||
function Node:set_dependents() end
|
||||
|
||||
function Node:set_argnodes(dict)
|
||||
if self.absolute_insert_position then
|
||||
-- append+remove "node" from absolute_insert_position to quickly create
|
||||
-- key for dict.
|
||||
table.insert(self.absolute_insert_position, "node")
|
||||
dict:set(self.absolute_insert_position, self)
|
||||
self.absolute_insert_position[#self.absolute_insert_position] = nil
|
||||
end
|
||||
if self.key then
|
||||
dict:set({ "key", self.key, "node" }, self)
|
||||
end
|
||||
end
|
||||
|
||||
function Node:make_args_absolute() end
|
||||
|
||||
function Node:resolve_position(position)
|
||||
error(
|
||||
string.format(
|
||||
"invalid resolve_position(%d) on node at %s",
|
||||
position,
|
||||
vim.inspect(self.absolute_position)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
function Node:static_init()
|
||||
self.static_visible = true
|
||||
end
|
||||
|
||||
-- resolve_*node*_ext_opts because snippet(Node)s have child_ext_opts, which
|
||||
-- also have to be resolved.
|
||||
-- This function generates a nodes ext_opts (those actually used in highlighting).
|
||||
function Node:resolve_node_ext_opts(base_prio, parent_ext_opts)
|
||||
if self.merge_node_ext_opts then
|
||||
self.ext_opts = ext_util.extend(
|
||||
vim.deepcopy(self.node_ext_opts),
|
||||
parent_ext_opts or self.parent.effective_child_ext_opts[self.type]
|
||||
)
|
||||
else
|
||||
self.ext_opts = self.node_ext_opts
|
||||
end
|
||||
|
||||
ext_util.set_abs_prio(
|
||||
self.ext_opts,
|
||||
(base_prio or self.parent.ext_opts.base_prio)
|
||||
+ session.config.ext_prio_increase
|
||||
)
|
||||
end
|
||||
|
||||
function Node:is_interactive()
|
||||
-- safe default.
|
||||
return true
|
||||
end
|
||||
|
||||
-- initialize active-setting in dry_run-table for `self`.
|
||||
function Node:init_dry_run_active(dry_run)
|
||||
if dry_run and dry_run.active[self] == nil then
|
||||
dry_run.active[self] = self.active
|
||||
end
|
||||
end
|
||||
-- determine whether this node is currently active.
|
||||
-- This is its own function (and not just a flat table-check) since we have to
|
||||
-- check the data in the dry_run-table or the node, depending on `dry_run`.
|
||||
function Node:is_active(dry_run)
|
||||
return (not dry_run and self.active) or (dry_run and dry_run.active[self])
|
||||
end
|
||||
|
||||
function Node:get_buf_position(opts)
|
||||
opts = opts or {}
|
||||
local raw = opts.raw ~= nil and opts.raw or true
|
||||
|
||||
if raw then
|
||||
return self.mark:pos_begin_end_raw()
|
||||
else
|
||||
return self.mark:pos_begin_end()
|
||||
end
|
||||
end
|
||||
|
||||
-- only does something for insert- and snippetNode.
|
||||
function Node:set_sibling_rgravs(_, _, _, _) end
|
||||
|
||||
-- when an insertNode receives text, its mark/region should contain all the
|
||||
-- text that is inserted.
|
||||
-- This can be achieved by setting the left and right "right-gravity"(rgrav) of
|
||||
-- the mark, which are responsible for controlling the direction an endpoint of
|
||||
-- the mark is moved when text is inserted.
|
||||
-- When a regular insertNode is focused/entered, we would like the left and
|
||||
-- right rgrav to be false and true, respectively. Example:
|
||||
-- this is an insertNodeAnd this is another insertNode
|
||||
-- mark1: l r
|
||||
-- mark2: l r
|
||||
-- if `this is an insertNode` should be focused, we have to set the rgrav of
|
||||
-- l1 false (because inserting text at the column of l1 should not shift l1 to
|
||||
-- the right). Similarly, the rgrav of r1 has to be set true, text inserted at
|
||||
-- its column SHOULD move it to the right.
|
||||
-- Complicating this whole thing: if like above there is an adjacent
|
||||
-- insertNode, its gravities have to be adjusted as well (if they are not, the
|
||||
-- insertNodes regions would overlap, which is obviously confusing). So, when
|
||||
-- adjusting some nodes rgravs, those of the siblings may have to be adjusted as well.
|
||||
-- Another example:
|
||||
-- aacc
|
||||
-- mark1: l r
|
||||
-- mark2: l
|
||||
-- r
|
||||
-- mark3: l r
|
||||
-- (the insertNode for mark2 is not visible at all, l2 and r2 are in the same
|
||||
-- column)
|
||||
-- This example highlights that not only the immediate sibling might need
|
||||
-- adjusting, but all siblings that share a mark-boundary with the node that
|
||||
-- should be focused.
|
||||
-- Even further complicating the matter: Snippets are trees, and failing to
|
||||
-- set the rgrav of snippet adjacent to (sharing an endpoint with) the node we
|
||||
-- want to focus, regardless of its position in the tree, will lead to extmarks
|
||||
-- covering the wrong regions.
|
||||
--
|
||||
-- More complications: focusing a node does not always mean setting the rgravs
|
||||
-- such that text will end up inside the node!
|
||||
-- For example, in the case of a terminating i(0) (like s("trig", {i(1,
|
||||
-- "text"), t" ", i(0)})), we would like to NOT include the text entered into
|
||||
-- it in the snippet. Thus, the gravities of it and all its parents have to be
|
||||
-- set (in this case) false,false, if the i(0) were at the beginning of the
|
||||
-- snippet (weird??) they'd have to be true,true.
|
||||
--
|
||||
--
|
||||
-- Unfortunately, we cannot guarantee that two extmarks on the same position
|
||||
-- also have the same gravities, for exmample if the text inside a focused node
|
||||
-- is deleted, and then another unrelated node is focused, the two endpoints of
|
||||
-- the previously focused node will have opposing rgravs.
|
||||
-- Maybe this whole procedure could be sped up further if we can assume that
|
||||
-- identical endpoints imply identical rgravs.
|
||||
local function focus_node(self, lrgrav, rrgrav)
|
||||
-- find nodes on path from self to root.
|
||||
local nodes_path = node_util.root_path(self)
|
||||
|
||||
-- direction is the direction away from this node, towards the outside of
|
||||
-- the tree-representation of the snippet.
|
||||
-- This is dubbed "direction" because it is the direction we will search in
|
||||
-- to find nodes on one endpoint of self.
|
||||
for _, direction in ipairs({ -1, 1 }) do
|
||||
local self_direction_endpoint = self.mark:get_endpoint(direction)
|
||||
local direction_rgrav = util.ternary(direction == -1, lrgrav, rrgrav)
|
||||
local effective_direction_rgrav = direction_rgrav
|
||||
|
||||
-- adjust left rgrav of all nodes on path upwards to root/snippet:
|
||||
-- (i st. self and the snippet are both handled)
|
||||
for i = 1, #nodes_path do
|
||||
local node = nodes_path[i]
|
||||
local node_direction_endpoint = node.mark:get_endpoint(direction)
|
||||
|
||||
if
|
||||
not util.pos_equal(
|
||||
node_direction_endpoint,
|
||||
self_direction_endpoint
|
||||
)
|
||||
then
|
||||
-- stop adjusting rgravs once self no longer is on the boundary of
|
||||
-- its parents, or if the rgrav is already set correctly.
|
||||
break
|
||||
end
|
||||
|
||||
node.mark:set_rgrav(direction, effective_direction_rgrav)
|
||||
|
||||
-- Once self's snippet is reached on the root-path, we will only
|
||||
-- adjust nodes self should be completely contained inside.
|
||||
-- Since the rgravs, however, may be set up otherwise (for example
|
||||
-- when focusing on an $0 that is the last node of the snippet), we
|
||||
-- have to adjust them now.
|
||||
if node.snippet == node then
|
||||
effective_direction_rgrav = direction == 1
|
||||
end
|
||||
|
||||
-- can't use node.parent, since that might skip nodes (in the case of
|
||||
-- dynamicNode, for example, the generated snippets parent is not the
|
||||
-- dynamicNode, but its parent).
|
||||
-- also: don't need to check for nil, because the
|
||||
local node_above = nodes_path[i + 1]
|
||||
if node_above then
|
||||
node_above:set_sibling_rgravs(
|
||||
node,
|
||||
self_direction_endpoint,
|
||||
direction,
|
||||
effective_direction_rgrav
|
||||
)
|
||||
end
|
||||
end
|
||||
self:subtree_set_pos_rgrav(
|
||||
self_direction_endpoint,
|
||||
-direction,
|
||||
direction_rgrav
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
function Node:subtree_set_rgrav(rgrav)
|
||||
self.mark:set_rgravs(rgrav, rgrav)
|
||||
end
|
||||
|
||||
function Node:subtree_set_pos_rgrav(_, direction, rgrav)
|
||||
self.mark:set_rgrav(-direction, rgrav)
|
||||
end
|
||||
|
||||
function Node:focus()
|
||||
focus_node(self, false, true)
|
||||
end
|
||||
|
||||
function Node:set_text(text)
|
||||
self:focus()
|
||||
|
||||
local node_from, node_to = self.mark:pos_begin_end_raw()
|
||||
local ok = pcall(
|
||||
vim.api.nvim_buf_set_text,
|
||||
0,
|
||||
node_from[1],
|
||||
node_from[2],
|
||||
node_to[1],
|
||||
node_to[2],
|
||||
text
|
||||
)
|
||||
-- we can assume that (part of) the snippet was deleted; remove it from
|
||||
-- the jumplist.
|
||||
if not ok then
|
||||
error("[LuaSnip Failed]: " .. vim.inspect(text))
|
||||
end
|
||||
end
|
||||
|
||||
-- since parents validate the adjacency, nodes where we don't know anything
|
||||
-- about the text inside them just have to assume they haven't been deleted :D
|
||||
function Node:extmarks_valid()
|
||||
return true
|
||||
end
|
||||
|
||||
function Node:linkable()
|
||||
-- linkable if insert or exitNode.
|
||||
return vim.tbl_contains(
|
||||
{ types.insertNode, types.exitNode },
|
||||
rawget(self, "type")
|
||||
)
|
||||
end
|
||||
function Node:interactive()
|
||||
-- interactive if immediately inside choiceNode.
|
||||
return vim.tbl_contains(
|
||||
{ types.insertNode, types.exitNode },
|
||||
rawget(self, "type")
|
||||
) or rawget(self, "choice") ~= nil
|
||||
end
|
||||
function Node:leaf()
|
||||
return vim.tbl_contains(
|
||||
{ types.textNode, types.functionNode, types.insertNode, types.exitNode },
|
||||
rawget(self, "type")
|
||||
)
|
||||
end
|
||||
|
||||
return {
|
||||
Node = Node,
|
||||
focus_node = focus_node,
|
||||
}
|
||||
@ -0,0 +1,307 @@
|
||||
-- restoreNode is implemented similarly to dynamicNode, only that it gets the snippetNode not from some function, but from self.snip.stored[key].
|
||||
|
||||
local Node = require("luasnip.nodes.node").Node
|
||||
local wrap_nodes_in_snippetNode =
|
||||
require("luasnip.nodes.snippet").wrap_nodes_in_snippetNode
|
||||
local RestoreNode = Node:new()
|
||||
local types = require("luasnip.util.types")
|
||||
local events = require("luasnip.util.events")
|
||||
local util = require("luasnip.util.util")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local mark = require("luasnip.util.mark").mark
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
|
||||
local function R(pos, key, nodes, opts)
|
||||
-- don't create nested snippetNodes, unnecessary.
|
||||
nodes = nodes and wrap_nodes_in_snippetNode(nodes)
|
||||
|
||||
return RestoreNode:new({
|
||||
pos = pos,
|
||||
key = key,
|
||||
mark = nil,
|
||||
snip = nodes,
|
||||
type = types.restoreNode,
|
||||
dependents = {},
|
||||
-- TODO: find out why it's necessary only for this node.
|
||||
active = false,
|
||||
}, opts)
|
||||
end
|
||||
extend_decorator.register(R, { arg_indx = 4 })
|
||||
|
||||
function RestoreNode:exit()
|
||||
if not self.visible then
|
||||
-- already exited.
|
||||
return
|
||||
end
|
||||
|
||||
self.visible = false
|
||||
self.mark:clear()
|
||||
-- snip should exist if exit is called.
|
||||
self.snip:store()
|
||||
-- will be copied on restore, no need to copy here too.
|
||||
self.parent.snippet.stored[self.key] = self.snip
|
||||
self.snip:exit()
|
||||
self.snip = nil
|
||||
self.active = false
|
||||
end
|
||||
|
||||
function RestoreNode:input_enter(_, dry_run)
|
||||
if dry_run then
|
||||
dry_run.active[self] = true
|
||||
return
|
||||
end
|
||||
|
||||
self.active = true
|
||||
self.visited = true
|
||||
self.mark:update_opts(self.ext_opts.active)
|
||||
|
||||
self:event(events.enter)
|
||||
end
|
||||
|
||||
function RestoreNode:input_leave(_, dry_run)
|
||||
if dry_run then
|
||||
dry_run.active[self] = false
|
||||
return
|
||||
end
|
||||
|
||||
self:event(events.leave)
|
||||
|
||||
self:update_dependents()
|
||||
self.active = false
|
||||
|
||||
self.mark:update_opts(self:get_passive_ext_opts())
|
||||
end
|
||||
|
||||
-- set snippetNode for this key here.
|
||||
function RestoreNode:subsnip_init()
|
||||
-- don't overwrite potentially stored snippetNode.
|
||||
-- due to metatable, there will always be a node set, but only those set
|
||||
-- by it (should) have the is_default set to true.
|
||||
if self.parent.snippet.stored[self.key].is_default and self.snip then
|
||||
self.parent.snippet.stored[self.key] = self.snip
|
||||
end
|
||||
end
|
||||
|
||||
-- don't need these, will be done in put_initial and get_static/docstring.
|
||||
function RestoreNode:indent(_) end
|
||||
|
||||
function RestoreNode:expand_tabs(_) end
|
||||
|
||||
-- will be called when before expansion but after snip.parent was initialized.
|
||||
-- Get the actual snippetNode here.
|
||||
function RestoreNode:put_initial(pos)
|
||||
local tmp = self.parent.snippet.stored[self.key]
|
||||
|
||||
-- act as if snip is directly inside parent.
|
||||
tmp.parent = self.parent
|
||||
tmp.indx = self.indx
|
||||
|
||||
tmp.next = self
|
||||
tmp.prev = self
|
||||
|
||||
tmp.snippet = self.parent.snippet
|
||||
|
||||
tmp.restore_node = self
|
||||
tmp.update_dependents = function(node)
|
||||
node:_update_dependents()
|
||||
-- self is restoreNode.
|
||||
node.restore_node:update_dependents()
|
||||
end
|
||||
|
||||
tmp:resolve_child_ext_opts()
|
||||
tmp:resolve_node_ext_opts()
|
||||
tmp:subsnip_init()
|
||||
|
||||
tmp:init_positions(self.snip_absolute_position)
|
||||
tmp:init_insert_positions(self.snip_absolute_insert_position)
|
||||
|
||||
tmp:make_args_absolute()
|
||||
|
||||
tmp:set_dependents()
|
||||
tmp:set_argnodes(self.parent.snippet.dependents_dict)
|
||||
|
||||
if vim.bo.expandtab then
|
||||
tmp:expand_tabs(util.tab_width(), self.parent.indentstring)
|
||||
end
|
||||
|
||||
-- correctly set extmark for node.
|
||||
-- does not modify ext_opts[node.type].
|
||||
local mark_opts = vim.tbl_extend("keep", {
|
||||
right_gravity = false,
|
||||
end_right_gravity = false,
|
||||
}, tmp:get_passive_ext_opts())
|
||||
|
||||
local old_pos = vim.deepcopy(pos)
|
||||
tmp:put_initial(pos)
|
||||
tmp.mark = mark(old_pos, pos, mark_opts)
|
||||
|
||||
-- no need to call update here, will be done by function calling this
|
||||
-- function.
|
||||
|
||||
self.snip = tmp
|
||||
self.visible = true
|
||||
end
|
||||
|
||||
-- the same as DynamicNode.
|
||||
function RestoreNode:jump_into(dir, no_move, dry_run)
|
||||
self:init_dry_run_active(dry_run)
|
||||
|
||||
if self:is_active(dry_run) then
|
||||
self:input_leave(no_move, dry_run)
|
||||
|
||||
if dir == 1 then
|
||||
return self.next:jump_into(dir, no_move, dry_run)
|
||||
else
|
||||
return self.prev:jump_into(dir, no_move, dry_run)
|
||||
end
|
||||
else
|
||||
self:input_enter(no_move, dry_run)
|
||||
|
||||
return self.snip:jump_into(dir, no_move, dry_run)
|
||||
end
|
||||
end
|
||||
|
||||
function RestoreNode:set_ext_opts(name)
|
||||
Node.set_ext_opts(self, name)
|
||||
|
||||
self.snip:set_ext_opts(name)
|
||||
end
|
||||
|
||||
function RestoreNode:update()
|
||||
self.snip:update()
|
||||
end
|
||||
|
||||
function RestoreNode:update_static()
|
||||
-- *_static-methods can use the stored snippet, since they don't require
|
||||
-- the snip to actually be inside the restoreNode.
|
||||
self.parent.snippet.stored[self.key]:update_static()
|
||||
end
|
||||
|
||||
local function snip_init(self, snip)
|
||||
snip.parent = self.parent
|
||||
|
||||
snip.snippet = self.parent.snippet
|
||||
-- pos should be nil if the restoreNode is inside a choiceNode.
|
||||
snip.pos = rawget(self, "pos")
|
||||
|
||||
snip:resolve_child_ext_opts()
|
||||
snip:resolve_node_ext_opts()
|
||||
snip:subsnip_init()
|
||||
|
||||
snip:init_positions(self.snip_absolute_position)
|
||||
snip:init_insert_positions(self.snip_absolute_insert_position)
|
||||
|
||||
snip:make_args_absolute()
|
||||
|
||||
snip:set_dependents()
|
||||
snip:set_argnodes(self.parent.snippet.dependents_dict)
|
||||
|
||||
snip:static_init()
|
||||
end
|
||||
|
||||
function RestoreNode:static_init()
|
||||
Node.static_init(self)
|
||||
self.snip = self.parent.snippet.stored[self.key]
|
||||
snip_init(self, self.snip)
|
||||
end
|
||||
|
||||
function RestoreNode:get_static_text()
|
||||
-- cache static_text, no need to recalculate function.
|
||||
if not self.static_text then
|
||||
self.static_text =
|
||||
self.parent.snippet.stored[self.key]:get_static_text()
|
||||
end
|
||||
return self.static_text
|
||||
end
|
||||
|
||||
function RestoreNode:get_docstring()
|
||||
if not self.docstring then
|
||||
self.docstring = self.parent.snippet.stored[self.key]:get_docstring()
|
||||
end
|
||||
return self.docstring
|
||||
end
|
||||
|
||||
function RestoreNode:store() end
|
||||
|
||||
-- will be restored through other means.
|
||||
function RestoreNode:update_restore()
|
||||
self.snip:update_restore()
|
||||
end
|
||||
|
||||
function RestoreNode:find_node(predicate)
|
||||
if self.snip then
|
||||
if predicate(self.snip) then
|
||||
return self.snip
|
||||
else
|
||||
return self.snip:find_node(predicate)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function RestoreNode:insert_to_node_absolute(position)
|
||||
if #position == 0 then
|
||||
return self.absolute_position
|
||||
end
|
||||
-- nil if not yet available.
|
||||
return self.snip and self.snip:insert_to_node_absolute(position)
|
||||
end
|
||||
|
||||
function RestoreNode:update_all_dependents()
|
||||
self:_update_dependents()
|
||||
self.snip:update_all_dependents()
|
||||
end
|
||||
|
||||
function RestoreNode:update_all_dependents_static()
|
||||
self:_update_dependents_static()
|
||||
self.parent.snippet.stored[self.key]:_update_dependents_static()
|
||||
end
|
||||
|
||||
function RestoreNode:init_insert_positions(position_so_far)
|
||||
Node.init_insert_positions(self, position_so_far)
|
||||
self.snip_absolute_insert_position =
|
||||
vim.deepcopy(self.absolute_insert_position)
|
||||
-- nodes of current snippet should have a 0 before.
|
||||
self.snip_absolute_insert_position[#self.snip_absolute_insert_position + 1] =
|
||||
0
|
||||
end
|
||||
|
||||
function RestoreNode:init_positions(position_so_far)
|
||||
Node.init_positions(self, position_so_far)
|
||||
self.snip_absolute_position = vim.deepcopy(self.absolute_position)
|
||||
-- Reach current snippet as snip_absolute_position..0.
|
||||
self.snip_absolute_position[#self.snip_absolute_position + 1] = 0
|
||||
end
|
||||
|
||||
function RestoreNode:resolve_position(position)
|
||||
-- position must be 0, there are no other options.
|
||||
return self.snip
|
||||
end
|
||||
|
||||
function RestoreNode:is_interactive()
|
||||
-- shouldn't be called, but revisit this once is_interactive is used in
|
||||
-- places other than lsp-snippets.
|
||||
return true
|
||||
end
|
||||
|
||||
function RestoreNode:subtree_set_pos_rgrav(pos, direction, rgrav)
|
||||
self.mark:set_rgrav(-direction, rgrav)
|
||||
if self.snip then
|
||||
self.snip:subtree_set_pos_rgrav(pos, direction, rgrav)
|
||||
end
|
||||
end
|
||||
|
||||
function RestoreNode:subtree_set_rgrav(rgrav)
|
||||
self.mark:set_rgravs(rgrav, rgrav)
|
||||
if self.snip then
|
||||
self.snip:subtree_set_rgrav(rgrav)
|
||||
end
|
||||
end
|
||||
|
||||
function RestoreNode:extmarks_valid()
|
||||
return node_util.generic_extmarks_valid(self, self.snip)
|
||||
end
|
||||
|
||||
return {
|
||||
R = R,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,125 @@
|
||||
-- the idea of this class is to lazily parse snippet (eg. only on expansion).
|
||||
--
|
||||
-- This is achieved by returning a proxy that has enough information to tell
|
||||
-- whether the snippet should be expanded at a given point (eg. all fields
|
||||
-- necessary to perform Snippet:matches()), but doesn't actually
|
||||
-- have to parse the snippet, leaving up-front cost of loading a bunch of
|
||||
-- snippets at a minimum.
|
||||
|
||||
local lsp_parse_fn = require("luasnip.util.parser").parse_snippet
|
||||
local snip_mod = require("luasnip.nodes.snippet")
|
||||
local node_util = require("luasnip.nodes.util")
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
|
||||
local SnippetProxy = {}
|
||||
|
||||
-- add Snippet-functions SnippetProxy can perform using the available data.
|
||||
SnippetProxy.matches = snip_mod.Snippet.matches
|
||||
SnippetProxy.invalidate = snip_mod.Snippet.invalidate
|
||||
SnippetProxy.retrieve_all = snip_mod.Snippet.retrieve_all
|
||||
|
||||
function SnippetProxy:get_docstring()
|
||||
return self.docstring
|
||||
end
|
||||
|
||||
function SnippetProxy:instantiate(parse_fn)
|
||||
-- self already contains initialized context and opts, can just be passed
|
||||
-- here, no problem.
|
||||
-- Bonus: if some keys are set on the snippets in the table (from the
|
||||
-- outside, for whatever reason), they are also present in the expanded
|
||||
-- snippet.
|
||||
--
|
||||
-- _S will copy self, so we can safely mutate (set metatables).
|
||||
local snippet = snip_mod._S(self, parse_fn(nil, self._snippet_string))
|
||||
-- snippet will have snippetProxies `copy`, nil it in snippet so it calls
|
||||
-- snippet-copy via metatable.
|
||||
snippet.copy = nil
|
||||
|
||||
self._snippet = snippet
|
||||
-- directly call into snippet on missing keys.
|
||||
setmetatable(self, {
|
||||
__index = self._snippet,
|
||||
})
|
||||
|
||||
-- return snippet so it can provide a missing key.
|
||||
return snippet
|
||||
end
|
||||
|
||||
-- some values of the snippet are nil by default, list them here so snippets
|
||||
-- aren't instantiated because of them.
|
||||
local license_to_nil =
|
||||
{ priority = true, snippetType = true, _source = true, filetype = true }
|
||||
|
||||
-- context and opts are (almost) the same objects as in s(contex, nodes, opts), snippet is a string representing the snippet.
|
||||
-- opts can aditionally contain the key `parse_fn`, which will be used to parse
|
||||
-- the snippet. This is useful, since snipmate-snippets are parsed with a
|
||||
-- function than regular lsp-snippets.
|
||||
-- context can be nil, in that case the resulting object can't be inserted into
|
||||
-- the snippet-tables, but may be used after expansion (i.e. returned from
|
||||
-- snippet:copy)
|
||||
local function new(context, snippet, opts)
|
||||
opts = opts or {}
|
||||
|
||||
-- default to regular lsp-parse-function.
|
||||
local parse_fn = lsp_parse_fn
|
||||
|
||||
if opts.parse_fn then
|
||||
parse_fn = opts.parse_fn
|
||||
end
|
||||
-- "error": there should not be duplicate keys, don't silently overwrite/keep.
|
||||
local sp = vim.tbl_extend(
|
||||
"error",
|
||||
{},
|
||||
context
|
||||
and snip_mod.init_snippet_context(
|
||||
node_util.wrap_context(context),
|
||||
opts
|
||||
)
|
||||
or {},
|
||||
snip_mod.init_snippet_opts(opts),
|
||||
node_util.init_node_opts(opts)
|
||||
)
|
||||
|
||||
sp._snippet_string = snippet
|
||||
-- override docstring
|
||||
sp.docstring = snippet
|
||||
|
||||
setmetatable(sp, {
|
||||
__index = function(t, k)
|
||||
if license_to_nil[k] then
|
||||
-- k might be nil, return it.
|
||||
return nil
|
||||
end
|
||||
|
||||
if SnippetProxy[k] then
|
||||
-- if it is possible to perform this operation without actually parsing the snippet, just do it.
|
||||
return SnippetProxy[k]
|
||||
end
|
||||
local snip = SnippetProxy.instantiate(t, parse_fn)
|
||||
if k == "_snippet" then
|
||||
return snip
|
||||
else
|
||||
return snip[k]
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- snippetProxy has to be able to return snippet on copy even after parsing,
|
||||
-- when the metatable has been changed. Therefore: set copy in each instance
|
||||
-- of snippetProxy.
|
||||
function sp:copy()
|
||||
local copy = self._snippet:copy()
|
||||
copy.id = self.id
|
||||
|
||||
return copy
|
||||
end
|
||||
|
||||
return sp
|
||||
end
|
||||
extend_decorator.register(
|
||||
new,
|
||||
{ arg_indx = 1, extend = node_util.snippet_extend_context },
|
||||
{ arg_indx = 3 }
|
||||
)
|
||||
|
||||
return new
|
||||
@ -0,0 +1,71 @@
|
||||
local node_mod = require("luasnip.nodes.node")
|
||||
local util = require("luasnip.util.util")
|
||||
local types = require("luasnip.util.types")
|
||||
local events = require("luasnip.util.events")
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
|
||||
local TextNode = node_mod.Node:new()
|
||||
|
||||
local function T(static_text, opts)
|
||||
return TextNode:new({
|
||||
static_text = util.to_string_table(static_text),
|
||||
mark = nil,
|
||||
type = types.textNode,
|
||||
}, opts)
|
||||
end
|
||||
extend_decorator.register(T, { arg_indx = 2 })
|
||||
|
||||
function TextNode:input_enter(no_move, dry_run)
|
||||
if dry_run then
|
||||
return
|
||||
end
|
||||
|
||||
self.mark:update_opts(self.ext_opts.active)
|
||||
self.visited = true
|
||||
|
||||
if not no_move then
|
||||
local mark_begin_pos = self.mark:pos_begin_raw()
|
||||
if vim.fn.mode() == "i" then
|
||||
util.insert_move_on(mark_begin_pos)
|
||||
else
|
||||
vim.api.nvim_feedkeys(
|
||||
vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
|
||||
"n",
|
||||
true
|
||||
)
|
||||
util.normal_move_on_insert(mark_begin_pos)
|
||||
end
|
||||
end
|
||||
|
||||
self:event(events.enter, no_move)
|
||||
end
|
||||
|
||||
function TextNode:update_all_dependents() end
|
||||
|
||||
function TextNode:is_interactive()
|
||||
-- a resounding false.
|
||||
return false
|
||||
end
|
||||
|
||||
function TextNode:extmarks_valid()
|
||||
local from, to = self.mark:pos_begin_end_raw()
|
||||
if
|
||||
util.pos_cmp(from, to) == 0
|
||||
and not (
|
||||
#self.static_text == 0
|
||||
or (#self.static_text == 1 and #self.static_text[1] == 0)
|
||||
)
|
||||
then
|
||||
-- assume the snippet is invalid if a textNode occupies zero space,
|
||||
-- but has text which would occupy some.
|
||||
-- This should allow some modifications, but as soon as a textNode is
|
||||
-- deleted entirely, we sound the alarm :D
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return {
|
||||
T = T,
|
||||
textNode = TextNode,
|
||||
}
|
||||
@ -0,0 +1,789 @@
|
||||
local util = require("luasnip.util.util")
|
||||
local ext_util = require("luasnip.util.ext_opts")
|
||||
local types = require("luasnip.util.types")
|
||||
local key_indexer = require("luasnip.nodes.key_indexer")
|
||||
local session = require("luasnip.session")
|
||||
|
||||
local function subsnip_init_children(parent, children)
|
||||
for _, child in ipairs(children) do
|
||||
if child.type == types.snippetNode then
|
||||
child.snippet = parent.snippet
|
||||
child:resolve_child_ext_opts()
|
||||
end
|
||||
child:resolve_node_ext_opts()
|
||||
child:subsnip_init()
|
||||
end
|
||||
end
|
||||
|
||||
local function init_child_positions_func(
|
||||
key,
|
||||
node_children_key,
|
||||
child_func_name
|
||||
)
|
||||
-- maybe via load()?
|
||||
return function(node, position_so_far)
|
||||
node[key] = vim.deepcopy(position_so_far)
|
||||
local pos_depth = #position_so_far + 1
|
||||
|
||||
for indx, child in ipairs(node[node_children_key]) do
|
||||
position_so_far[pos_depth] = indx
|
||||
child[child_func_name](child, position_so_far)
|
||||
end
|
||||
-- undo changes to position_so_far.
|
||||
position_so_far[pos_depth] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function make_args_absolute(args, parent_insert_position, target)
|
||||
for i, arg in ipairs(args) do
|
||||
if type(arg) == "number" then
|
||||
-- the arg is a number, should be interpreted relative to direct
|
||||
-- parent.
|
||||
local t = vim.deepcopy(parent_insert_position)
|
||||
table.insert(t, arg)
|
||||
target[i] = { absolute_insert_position = t }
|
||||
else
|
||||
-- insert node, absolute_indexer, or key itself, node's
|
||||
-- absolute_insert_position may be nil, check for that during
|
||||
-- usage.
|
||||
target[i] = arg
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function wrap_args(args)
|
||||
-- stylua: ignore
|
||||
if type(args) ~= "table" or
|
||||
(type(args) == "table" and args.absolute_insert_position) or
|
||||
key_indexer.is_key(args) then
|
||||
-- args is one single arg, wrap it.
|
||||
return { args }
|
||||
else
|
||||
return args
|
||||
end
|
||||
end
|
||||
|
||||
-- includes child, does not include parent.
|
||||
local function get_nodes_between(parent, child)
|
||||
local nodes = {}
|
||||
|
||||
-- special case for nodes without absolute_position (which is only
|
||||
-- start_node).
|
||||
if child.pos == -1 then
|
||||
-- no nodes between, only child.
|
||||
nodes[1] = child
|
||||
return nodes
|
||||
end
|
||||
|
||||
local child_pos = child.absolute_position
|
||||
|
||||
local indx = #parent.absolute_position + 1
|
||||
local prev = parent
|
||||
while child_pos[indx] do
|
||||
local next = prev:resolve_position(child_pos[indx])
|
||||
nodes[#nodes + 1] = next
|
||||
prev = next
|
||||
indx = indx + 1
|
||||
end
|
||||
|
||||
return nodes
|
||||
end
|
||||
|
||||
-- assumes that children of child are not even active.
|
||||
-- If they should also be left, do that separately.
|
||||
-- Does not leave the parent.
|
||||
local function leave_nodes_between(parent, child, no_move)
|
||||
local nodes = get_nodes_between(parent, child)
|
||||
if #nodes == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- reverse order, leave child first.
|
||||
for i = #nodes, 2, -1 do
|
||||
-- this only happens for nodes where the parent will also be left
|
||||
-- entirely (because we stop at nodes[2], and handle nodes[1]
|
||||
-- separately)
|
||||
nodes[i]:input_leave(no_move)
|
||||
nodes[i - 1]:input_leave_children()
|
||||
end
|
||||
nodes[1]:input_leave(no_move)
|
||||
end
|
||||
|
||||
local function enter_nodes_between(parent, child, no_move)
|
||||
local nodes = get_nodes_between(parent, child)
|
||||
if #nodes == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
for i = 1, #nodes - 1 do
|
||||
-- only enter children for nodes before the last (lowest) one.
|
||||
nodes[i]:input_enter(no_move)
|
||||
nodes[i]:input_enter_children()
|
||||
end
|
||||
nodes[#nodes]:input_enter(no_move)
|
||||
end
|
||||
|
||||
local function select_node(node)
|
||||
local node_begin, node_end = node.mark:pos_begin_end_raw()
|
||||
util.any_select(node_begin, node_end)
|
||||
end
|
||||
|
||||
local function print_dict(dict)
|
||||
print(vim.inspect(dict, {
|
||||
process = function(item, path)
|
||||
if path[#path] == "node" or path[#path] == "dependent" then
|
||||
return "node@" .. vim.inspect(item.absolute_position)
|
||||
elseif path[#path] ~= vim.inspect.METATABLE then
|
||||
return item
|
||||
end
|
||||
end,
|
||||
}))
|
||||
end
|
||||
|
||||
local function init_node_opts(opts)
|
||||
local in_node = {}
|
||||
if not opts then
|
||||
opts = {}
|
||||
end
|
||||
|
||||
-- copy once here, the opts might be reused.
|
||||
in_node.node_ext_opts =
|
||||
ext_util.complete(vim.deepcopy(opts.node_ext_opts or {}))
|
||||
|
||||
if opts.merge_node_ext_opts == nil then
|
||||
in_node.merge_node_ext_opts = true
|
||||
else
|
||||
in_node.merge_node_ext_opts = opts.merge_node_ext_opts
|
||||
end
|
||||
|
||||
in_node.key = opts.key
|
||||
|
||||
in_node.node_callbacks = opts.node_callbacks or {}
|
||||
|
||||
return in_node
|
||||
end
|
||||
|
||||
local function snippet_extend_context(arg, extend)
|
||||
if type(arg) == "string" then
|
||||
arg = { trig = arg }
|
||||
end
|
||||
|
||||
-- both are table or nil now.
|
||||
return vim.tbl_extend("keep", arg or {}, extend or {})
|
||||
end
|
||||
|
||||
local function wrap_context(context)
|
||||
if type(context) == "string" then
|
||||
return { trig = context }
|
||||
else
|
||||
return context
|
||||
end
|
||||
end
|
||||
|
||||
local function linkable_node(node)
|
||||
-- node.type has to be one of insertNode, exitNode.
|
||||
return vim.tbl_contains(
|
||||
{ types.insertNode, types.exitNode },
|
||||
rawget(node, "type")
|
||||
)
|
||||
end
|
||||
|
||||
-- mainly used internally, by binarysearch_pos.
|
||||
-- these are the nodes that are definitely not linkable, there are nodes like
|
||||
-- dynamicNode or snippetNode that might be linkable, depending on their
|
||||
-- content. Could look into that to make this more complete, but that does not
|
||||
-- feel appropriate (higher runtime), most cases should be served well by this
|
||||
-- heuristic.
|
||||
local function non_linkable_node(node)
|
||||
return vim.tbl_contains(
|
||||
{ types.textNode, types.functionNode },
|
||||
rawget(node, "type")
|
||||
)
|
||||
end
|
||||
-- return whether a node is certainly (not) interactive.
|
||||
-- Coincindentially, the same nodes as (non-)linkable ones, but since there is a
|
||||
-- semantic difference, use separate names.
|
||||
local interactive_node = linkable_node
|
||||
local non_interactive_node = non_linkable_node
|
||||
|
||||
local function prefer_nodes(prefer_func, reject_func)
|
||||
return function(cmp_mid_to, cmp_mid_from, mid_node)
|
||||
local reject_mid = reject_func(mid_node)
|
||||
local prefer_mid = prefer_func(mid_node)
|
||||
|
||||
-- if we can choose which node to continue in, prefer the one that
|
||||
-- may be linkable/interactive.
|
||||
if cmp_mid_to == 0 and reject_mid then
|
||||
return true, false
|
||||
elseif cmp_mid_from == 0 and reject_mid then
|
||||
return false, true
|
||||
elseif (cmp_mid_to == 0 or cmp_mid_from == 0) and prefer_mid then
|
||||
return false, false
|
||||
else
|
||||
return cmp_mid_to >= 0, cmp_mid_from < 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- functions for resolving conflicts, if `pos` is on the boundary of two nodes.
|
||||
-- Return whether to continue behind or before mid (in that order).
|
||||
-- At most one of those may be true, of course.
|
||||
local binarysearch_preference = {
|
||||
outside = function(cmp_mid_to, cmp_mid_from, _)
|
||||
return cmp_mid_to >= 0, cmp_mid_from <= 0
|
||||
end,
|
||||
inside = function(cmp_mid_to, cmp_mid_from, _)
|
||||
return cmp_mid_to > 0, cmp_mid_from < 0
|
||||
end,
|
||||
linkable = prefer_nodes(linkable_node, non_linkable_node),
|
||||
interactive = prefer_nodes(interactive_node, non_interactive_node),
|
||||
}
|
||||
-- `nodes` is a list of nodes ordered by their occurrence in the buffer.
|
||||
-- `pos` is a row-column-tuble, byte-columns, and we return the node the LEFT
|
||||
-- EDGE(/side) of `pos` is inside.
|
||||
-- This convention is chosen since a snippet inserted at `pos` will move the
|
||||
-- character at `pos` to the right.
|
||||
-- The exact meaning of "inside" can be influenced with `respect_rgravs` and
|
||||
-- `boundary_resolve_mode`:
|
||||
-- * if `respect_rgravs` is true, "inside" emulates the shifting-behaviour of
|
||||
-- extmarks:
|
||||
-- First of all, we compare the left edge of `pos` with the left/right edges
|
||||
-- of from/to, depending on rgrav.
|
||||
-- If the left edge is <= left/right edge of from, and < left/right edge of
|
||||
-- to, `pos` is inside the node.
|
||||
--
|
||||
-- * if `respect_rgravs` is false, pos has to be fully inside a node to be
|
||||
-- considered inside it. If pos is on the left endpoint, it is considered to be
|
||||
-- left of the node, and likewise for the right endpoint.
|
||||
--
|
||||
-- * `boundary_resolve_mode` changes how a position on the boundary of a node
|
||||
-- is treated:
|
||||
-- * for `"prefer_linkable/interactive"`, we assume that the nodes in `nodes` are
|
||||
-- contiguous, and prefer falling into the previous/next node if `pos` is on
|
||||
-- mid's boundary, and mid is not linkable/interactie.
|
||||
-- This way, we are more likely to return a node that can handle a new
|
||||
-- snippet/is interactive.
|
||||
-- * `"prefer_outside"` makes sense when the nodes are not contiguous, and we'd
|
||||
-- like to find a position between two nodes.
|
||||
-- This mode makes sense for finding the snippet a new snippet should be
|
||||
-- inserted in, since we'd like to prefer inserting before/after a snippet, if
|
||||
-- the position is ambiguous.
|
||||
--
|
||||
-- In general:
|
||||
-- These options are useful for making this function more general: When
|
||||
-- searching in the contiguous nodes of a snippet, we'd like this routine to
|
||||
-- return any of them (obviously the one pos is inside/or on the border of, and
|
||||
-- we'd like to prefer returning a node that can be linked), but in no case
|
||||
-- fail.
|
||||
-- However! when searching the top-level snippets with the intention of finding
|
||||
-- the snippet/node a new snippet should be expanded inside, it seems better to
|
||||
-- shift an existing snippet to the right/left than expand the new snippet
|
||||
-- inside it (when the expand-point is on the boundary).
|
||||
local function binarysearch_pos(
|
||||
nodes,
|
||||
pos,
|
||||
respect_rgravs,
|
||||
boundary_resolve_mode
|
||||
)
|
||||
local left = 1
|
||||
local right = #nodes
|
||||
|
||||
-- actual search-routine from
|
||||
-- https://github.com/Roblox/Wiki-Lua-Libraries/blob/master/StandardLibraries/BinarySearch.lua
|
||||
if #nodes == 0 then
|
||||
return nil, 1
|
||||
end
|
||||
while true do
|
||||
local mid = left + math.floor((right - left) / 2)
|
||||
local mid_mark = nodes[mid].mark
|
||||
local ok, mid_from, mid_to = pcall(mid_mark.pos_begin_end_raw, mid_mark)
|
||||
|
||||
if not ok then
|
||||
-- error while running this procedure!
|
||||
-- return false (because I don't know how to do this with `error`
|
||||
-- and the offending node).
|
||||
-- (returning data instead of a message in `error` seems weird..)
|
||||
return false, mid
|
||||
end
|
||||
|
||||
if respect_rgravs then
|
||||
-- if rgrav is set on either endpoint, the node considers its
|
||||
-- endpoint to be the right, not the left edge.
|
||||
-- We only want to work with left edges but since the right edge is
|
||||
-- the left edge of the next column, this is not an issue :)
|
||||
-- TODO: does this fail with multibyte characters???
|
||||
if mid_mark:get_rgrav(-1) then
|
||||
mid_from[2] = mid_from[2] + 1
|
||||
end
|
||||
if mid_mark:get_rgrav(1) then
|
||||
mid_to[2] = mid_to[2] + 1
|
||||
end
|
||||
end
|
||||
|
||||
local cmp_mid_to = util.pos_cmp(pos, mid_to)
|
||||
local cmp_mid_from = util.pos_cmp(pos, mid_from)
|
||||
|
||||
local cont_behind_mid, cont_before_mid =
|
||||
boundary_resolve_mode(cmp_mid_to, cmp_mid_from, nodes[mid])
|
||||
|
||||
if cont_behind_mid then
|
||||
-- make sure right-left becomes smaller.
|
||||
left = mid + 1
|
||||
if left > right then
|
||||
return nil, mid + 1
|
||||
end
|
||||
elseif cont_before_mid then
|
||||
-- continue search on left side
|
||||
right = mid - 1
|
||||
if left > right then
|
||||
return nil, mid
|
||||
end
|
||||
else
|
||||
-- greater-equal than mid_from, smaller or equal to mid_to => left edge
|
||||
-- of pos is inside nodes[mid] :)
|
||||
return nodes[mid], mid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- a and b have to be in the same snippet, return their first (as seen from
|
||||
-- them) common parent.
|
||||
local function first_common_node(a, b)
|
||||
local a_pos = a.absolute_position
|
||||
local b_pos = b.absolute_position
|
||||
|
||||
-- last as seen from root.
|
||||
local i = 0
|
||||
local last_common = a.parent.snippet
|
||||
-- invariant: last_common is parent of both a and b.
|
||||
while (a_pos[i + 1] ~= nil) and a_pos[i + 1] == b_pos[i + 1] do
|
||||
last_common = last_common:resolve_position(a_pos[i + 1])
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
return last_common
|
||||
end
|
||||
|
||||
-- roots at depth 0, children of root at depth 1, their children at 2, ...
|
||||
local function snippettree_depth(snippet)
|
||||
local depth = 0
|
||||
while snippet.parent_node ~= nil do
|
||||
snippet = snippet.parent_node.parent.snippet
|
||||
depth = depth + 1
|
||||
end
|
||||
return depth
|
||||
end
|
||||
|
||||
-- find the first common snippet a and b have on their respective unique paths
|
||||
-- to the snippet-roots.
|
||||
-- if no common ancestor exists (ie. a and b are roots of their buffers'
|
||||
-- forest, or just in different trees), return nil.
|
||||
-- in both cases, the paths themselves are returned as well.
|
||||
-- The common ancestor is included in the paths, except if there is none.
|
||||
-- Instead of storing the snippets in the paths, they are represented by the
|
||||
-- node which contains the next-lower snippet in the path (or `from`/`to`, if it's
|
||||
-- the first node of the path)
|
||||
-- This is a bit complicated, but this representation contains more information
|
||||
-- (or, more easily accessible information) than storing snippets: the
|
||||
-- immediate parent of the child along the path cannot be easily retrieved if
|
||||
-- the snippet is stored, but the snippet can be easily retrieved if the child
|
||||
-- is stored (.parent.snippet).
|
||||
-- And, so far this is pretty specific to refocus, and thus modeled so there is
|
||||
-- very little additional work in that method.
|
||||
-- At most one of a,b may be nil.
|
||||
local function first_common_snippet_ancestor_path(a, b)
|
||||
local a_path = {}
|
||||
local b_path = {}
|
||||
|
||||
-- general idea: we find the depth of a and b, walk upward with the deeper
|
||||
-- one until we find its first ancestor with the same depth as the less
|
||||
-- deep snippet, and then follow both paths until they arrive at the same
|
||||
-- snippet (or at the root of their respective trees).
|
||||
-- if either is nil, we treat it like it's one of the roots (the code will
|
||||
-- behave correctly this way, and return an empty path for the nil-node,
|
||||
-- and the correct path for the non-nil one).
|
||||
local a_depth = a ~= nil and snippettree_depth(a) or 0
|
||||
local b_depth = b ~= nil and snippettree_depth(b) or 0
|
||||
|
||||
-- bit subtle: both could be 0, but one could be nil.
|
||||
-- deeper should not be nil! (this allows us to do the whole walk for the
|
||||
-- non-nil node in the first for-loop, as opposed to needing some special
|
||||
-- handling).
|
||||
local deeper, deeper_path, other, other_path
|
||||
if b == nil or (a ~= nil and a_depth > b_depth) then
|
||||
deeper = a
|
||||
other = b
|
||||
deeper_path = a_path
|
||||
other_path = b_path
|
||||
else
|
||||
-- we land here if `b ~= nil and (a == nil or a_depth >= b_depth)`, so
|
||||
-- exactly what we want.
|
||||
deeper = b
|
||||
other = a
|
||||
deeper_path = b_path
|
||||
other_path = a_path
|
||||
end
|
||||
|
||||
for _ = 1, math.abs(a_depth - b_depth) do
|
||||
table.insert(deeper_path, deeper.parent_node)
|
||||
deeper = deeper.parent_node.parent.snippet
|
||||
end
|
||||
-- here: deeper and other are at the same depth.
|
||||
-- If we walk upwards one step at a time, they will meet at the same
|
||||
-- parent, or hit their respective roots.
|
||||
|
||||
-- deeper can't be nil, if other is, we are done here and can return the
|
||||
-- paths (and there is no shared node)
|
||||
if other == nil then
|
||||
return nil, a_path, b_path
|
||||
end
|
||||
-- beyond here, deeper and other are not nil.
|
||||
|
||||
while deeper ~= other do
|
||||
if deeper.parent_node == nil then
|
||||
-- deeper is at depth 0 => other as well => both are roots.
|
||||
return nil, a_path, b_path
|
||||
end
|
||||
|
||||
table.insert(deeper_path, deeper.parent_node)
|
||||
table.insert(other_path, other.parent_node)
|
||||
|
||||
-- walk one step towards root.
|
||||
deeper = deeper.parent_node.parent.snippet
|
||||
other = other.parent_node.parent.snippet
|
||||
end
|
||||
|
||||
-- either one will do here.
|
||||
return deeper, a_path, b_path
|
||||
end
|
||||
|
||||
-- removes focus from `from` and upwards up to the first common ancestor
|
||||
-- (node!) of `from` and `to`, and then focuses nodes between that ancestor and
|
||||
-- `to`.
|
||||
-- Requires that `from` is currently entered/focused, and that no snippet
|
||||
-- between `to` and its root is invalid.
|
||||
local function refocus(from, to)
|
||||
if from == nil and to == nil then
|
||||
-- absolutely nothing to do, should not happen.
|
||||
return
|
||||
end
|
||||
-- pass nil if from/to is nil.
|
||||
-- if either is nil, first_common_node is nil, and the corresponding list empty.
|
||||
local first_common_snippet, from_snip_path, to_snip_path =
|
||||
first_common_snippet_ancestor_path(
|
||||
from and from.parent.snippet,
|
||||
to and to.parent.snippet
|
||||
)
|
||||
|
||||
-- we want leave/enter_path to be s.t. leaving/entering all nodes between
|
||||
-- each entry and its snippet (and the snippet itself) will leave/enter all
|
||||
-- nodes between the first common snippet (or the root-snippet) and
|
||||
-- from/to.
|
||||
-- Then, the nodes between the first common node and the respective
|
||||
-- entrypoints (also nodes) into the first common snippet will have to be
|
||||
-- left/entered, which is handled by final_leave_/first_enter_/common_node.
|
||||
|
||||
-- from, to are not yet in the paths.
|
||||
table.insert(from_snip_path, 1, from)
|
||||
table.insert(to_snip_path, 1, to)
|
||||
|
||||
-- determine how far to leave: if there is a common snippet, only up to the
|
||||
-- first common node of from and to, otherwise leave the one snippet, and
|
||||
-- enter the other completely.
|
||||
local final_leave_node, first_enter_node, common_node
|
||||
if first_common_snippet then
|
||||
-- there is a common snippet => there is a common node => we have to
|
||||
-- set final_leave_node, first_enter_node, and common_node.
|
||||
final_leave_node = from_snip_path[#from_snip_path]
|
||||
first_enter_node = to_snip_path[#to_snip_path]
|
||||
common_node = first_common_node(first_enter_node, final_leave_node)
|
||||
|
||||
-- Also remove these last nodes from the lists, their snippet is not
|
||||
-- supposed to be left entirely.
|
||||
from_snip_path[#from_snip_path] = nil
|
||||
to_snip_path[#to_snip_path] = nil
|
||||
end
|
||||
|
||||
-- now do leave/enter, set no_move on all operations.
|
||||
-- if one of from/to was nil, there are no leave/enter-operations done for
|
||||
-- it (from/to_snip_path is {}, final_leave/first_enter_* is nil).
|
||||
|
||||
-- leave_children on all from-nodes except the original from.
|
||||
if #from_snip_path > 0 then
|
||||
local ok1, ok2
|
||||
if from.type == types.exitNode then
|
||||
ok1 = pcall(from.input_leave, from, true)
|
||||
ok2 = true
|
||||
else
|
||||
-- we know that the first node is from.
|
||||
ok1 = pcall(leave_nodes_between, from.parent.snippet, from, true)
|
||||
-- leave_nodes_between does not affect snippet, so that has to be left
|
||||
-- here.
|
||||
-- snippet does not have input_leave_children, so only input_leave
|
||||
-- needs to be called.
|
||||
ok2 = pcall(
|
||||
from.parent.snippet.input_leave,
|
||||
from.parent.snippet,
|
||||
true
|
||||
)
|
||||
end
|
||||
if not ok1 or not ok2 then
|
||||
from.parent.snippet:remove_from_jumplist()
|
||||
end
|
||||
end
|
||||
for i = 2, #from_snip_path do
|
||||
local node = from_snip_path[i]
|
||||
local ok1, ok2, ok3
|
||||
ok1 = pcall(node.input_leave_children, node)
|
||||
|
||||
if node.type == types.exitNode then
|
||||
ok2 = pcall(node.input_leave, node, true)
|
||||
ok3 = true
|
||||
else
|
||||
ok2 = pcall(leave_nodes_between, node.parent.snippet, node, true)
|
||||
ok3 = pcall(
|
||||
node.parent.snippet.input_leave,
|
||||
node.parent.snippet,
|
||||
true
|
||||
)
|
||||
end
|
||||
|
||||
if not ok1 or not ok2 or not ok3 then
|
||||
from.parent.snippet:remove_from_jumplist()
|
||||
end
|
||||
end
|
||||
|
||||
-- this leave, and the following enters should be safe: the path to `to`
|
||||
-- was verified via extmarks_valid (precondition).
|
||||
if common_node and final_leave_node then
|
||||
-- if the final_leave_node is from, its children are not active (which
|
||||
-- stems from the requirement that from is the currently active node),
|
||||
-- and so don't have to be left.
|
||||
if final_leave_node ~= from then
|
||||
final_leave_node:input_leave_children()
|
||||
end
|
||||
leave_nodes_between(common_node, final_leave_node, true)
|
||||
end
|
||||
|
||||
if common_node and first_enter_node then
|
||||
-- In general we assume that common_node is active when we are here.
|
||||
-- This may not be the case if we are currently inside the i(0) or
|
||||
-- i(-1), since the snippet might be the common node and in this case,
|
||||
-- it is inactive.
|
||||
-- This means that, if we want to enter a non-exitNode, we have to
|
||||
-- explicitly activate the snippet for all jumps to behave correctly.
|
||||
-- (if we enter a i(0)/i(-1), this is not necessary, of course).
|
||||
if
|
||||
final_leave_node.type == types.exitNode
|
||||
and first_enter_node.type ~= types.exitNode
|
||||
then
|
||||
common_node:input_enter(true)
|
||||
end
|
||||
-- symmetrically, entering an i(0)/i(-1) requires leaving the snippet.
|
||||
if
|
||||
final_leave_node.type ~= types.exitNode
|
||||
and first_enter_node.type == types.exitNode
|
||||
then
|
||||
common_node:input_leave(true)
|
||||
end
|
||||
|
||||
enter_nodes_between(common_node, first_enter_node, true)
|
||||
|
||||
-- if the `first_enter_node` is already `to` (occurs if `to` is in the
|
||||
-- common snippet of to and from), we should not enter its children.
|
||||
-- (we only want to `input_enter` to.)
|
||||
if first_enter_node ~= to then
|
||||
first_enter_node:input_enter_children()
|
||||
end
|
||||
end
|
||||
|
||||
-- same here, input_enter_children has to be called manually for the
|
||||
-- to-nodes of the path we are entering (since enter_nodes_between does not
|
||||
-- call it for the child-node).
|
||||
|
||||
for i = #to_snip_path, 2, -1 do
|
||||
local node = to_snip_path[i]
|
||||
if node.type ~= types.exitNode then
|
||||
node.parent.snippet:input_enter(true)
|
||||
enter_nodes_between(node.parent.snippet, node, true)
|
||||
else
|
||||
node:input_enter(true)
|
||||
end
|
||||
node:input_enter_children()
|
||||
end
|
||||
if #to_snip_path > 0 then
|
||||
if to.type ~= types.exitNode then
|
||||
to.parent.snippet:input_enter(true)
|
||||
else
|
||||
to.parent.snippet:input_leave(true)
|
||||
end
|
||||
enter_nodes_between(to.parent.snippet, to, true)
|
||||
end
|
||||
|
||||
-- it may be that we only leave nodes in this process (happens if to is a
|
||||
-- parent of from).
|
||||
-- If that is the case, we will not explicitly focus on to, and it may be
|
||||
-- that focus is even lost if it was focused previously (leave may trigger
|
||||
-- update, update may change focus)
|
||||
-- To prevent this, just call focus here, which is pretty close to a noop
|
||||
-- if to is already focused.
|
||||
if to then
|
||||
to:focus()
|
||||
end
|
||||
end
|
||||
|
||||
local function generic_extmarks_valid(node, child)
|
||||
-- valid if
|
||||
-- - extmark-extents match.
|
||||
-- - current choice is valid
|
||||
local ok1, self_from, self_to =
|
||||
pcall(node.mark.pos_begin_end_raw, node.mark)
|
||||
local ok2, child_from, child_to =
|
||||
pcall(child.mark.pos_begin_end_raw, child.mark)
|
||||
|
||||
if
|
||||
not ok1
|
||||
or not ok2
|
||||
or util.pos_cmp(self_from, child_from) ~= 0
|
||||
or util.pos_cmp(self_to, child_to) ~= 0
|
||||
then
|
||||
return false
|
||||
end
|
||||
return child:extmarks_valid()
|
||||
end
|
||||
|
||||
-- returns: * the smallest known snippet `pos` is inside.
|
||||
-- * the list of other snippets inside the snippet of this smallest
|
||||
-- node
|
||||
-- * the index this snippet would be at if inserted into that list
|
||||
-- * the node of this snippet pos is on.
|
||||
local function snippettree_find_undamaged_node(pos, opts)
|
||||
local prev_parent, child_indx, found_parent
|
||||
local prev_parent_children =
|
||||
session.snippet_roots[vim.api.nvim_get_current_buf()]
|
||||
|
||||
while true do
|
||||
-- false: don't respect rgravs.
|
||||
-- Prefer inserting the snippet outside an existing one.
|
||||
found_parent, child_indx = binarysearch_pos(
|
||||
prev_parent_children,
|
||||
pos,
|
||||
opts.tree_respect_rgravs,
|
||||
opts.tree_preference
|
||||
)
|
||||
if found_parent == false then
|
||||
-- if the procedure returns false, there was an error getting the
|
||||
-- position of a node (in this case, that node is a snippet).
|
||||
-- The position of the offending snippet is returned in child_indx,
|
||||
-- and we can remove it here.
|
||||
prev_parent_children[child_indx]:remove_from_jumplist()
|
||||
elseif found_parent ~= nil and not found_parent:extmarks_valid() then
|
||||
-- found snippet damaged (the idea to sidestep the damaged snippet,
|
||||
-- even if no error occurred _right now_, is to ensure that we can
|
||||
-- input_enter all the nodes along the insertion-path correctly).
|
||||
found_parent:remove_from_jumplist()
|
||||
-- continue again with same parent, but one less snippet in its
|
||||
-- children => shouldn't cause endless loop.
|
||||
elseif found_parent == nil then
|
||||
break
|
||||
else
|
||||
prev_parent = found_parent
|
||||
-- can index prev_parent, since found_parent is not nil, and
|
||||
-- assigned to prev_parent.
|
||||
prev_parent_children = prev_parent.child_snippets
|
||||
end
|
||||
end
|
||||
|
||||
local node
|
||||
if prev_parent then
|
||||
-- if found, find node to insert at, prefer receiving a linkable node.
|
||||
node = prev_parent:node_at(pos, opts.snippet_mode)
|
||||
end
|
||||
|
||||
return prev_parent, prev_parent_children, child_indx, node
|
||||
end
|
||||
|
||||
local function root_path(node)
|
||||
local path = {}
|
||||
|
||||
while node do
|
||||
local node_snippet = node.parent.snippet
|
||||
local snippet_node_path = get_nodes_between(node_snippet, node)
|
||||
-- get_nodes_between gives parent -> node, but we need
|
||||
-- node -> parent => insert back to front.
|
||||
for i = #snippet_node_path, 1, -1 do
|
||||
table.insert(path, snippet_node_path[i])
|
||||
end
|
||||
-- parent not in get_nodes_between.
|
||||
table.insert(path, node_snippet)
|
||||
|
||||
node = node_snippet.parent_node
|
||||
end
|
||||
|
||||
return path
|
||||
end
|
||||
|
||||
-- adjust rgravs of siblings of the node with indx child_from_indx in nodes.
|
||||
local function nodelist_adjust_rgravs(
|
||||
nodes,
|
||||
child_from_indx,
|
||||
child_endpoint,
|
||||
direction,
|
||||
rgrav,
|
||||
nodes_adjacent
|
||||
)
|
||||
-- only handle siblings, not the node with child_from_indx itself.
|
||||
local i = child_from_indx
|
||||
local node = nodes[i]
|
||||
while node do
|
||||
local direction_node_endpoint = node.mark:get_endpoint(direction)
|
||||
if util.pos_equal(direction_node_endpoint, child_endpoint) then
|
||||
-- both endpoints of node are on top of child_endpoint (we wouldn't
|
||||
-- be in the loop with `node` if the -direction-endpoint didn't
|
||||
-- match), so update rgravs of the entire subtree to match rgrav
|
||||
node:subtree_set_rgrav(rgrav)
|
||||
else
|
||||
-- either assume that they are adjacent, or check.
|
||||
if
|
||||
nodes_adjacent
|
||||
or util.pos_equal(
|
||||
node.mark:get_endpoint(-direction),
|
||||
child_endpoint
|
||||
)
|
||||
then
|
||||
-- only the -direction-endpoint matches child_endpoint, adjust its
|
||||
-- position and break the loop (don't need to look at any other
|
||||
-- siblings).
|
||||
node:subtree_set_pos_rgrav(child_endpoint, direction, rgrav)
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
i = i + direction
|
||||
node = nodes[i]
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
subsnip_init_children = subsnip_init_children,
|
||||
init_child_positions_func = init_child_positions_func,
|
||||
make_args_absolute = make_args_absolute,
|
||||
wrap_args = wrap_args,
|
||||
wrap_context = wrap_context,
|
||||
get_nodes_between = get_nodes_between,
|
||||
leave_nodes_between = leave_nodes_between,
|
||||
enter_nodes_between = enter_nodes_between,
|
||||
select_node = select_node,
|
||||
print_dict = print_dict,
|
||||
init_node_opts = init_node_opts,
|
||||
snippet_extend_context = snippet_extend_context,
|
||||
linkable_node = linkable_node,
|
||||
binarysearch_pos = binarysearch_pos,
|
||||
binarysearch_preference = binarysearch_preference,
|
||||
refocus = refocus,
|
||||
generic_extmarks_valid = generic_extmarks_valid,
|
||||
snippettree_find_undamaged_node = snippettree_find_undamaged_node,
|
||||
interactive_node = interactive_node,
|
||||
root_path = root_path,
|
||||
nodelist_adjust_rgravs = nodelist_adjust_rgravs,
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
local jsregexp_compile_safe = require("luasnip.util.jsregexp")
|
||||
|
||||
-- generate nil-opts-instances here, and save them.
|
||||
-- This is to prevent generating 100s of the exact same function.
|
||||
local default_match_pattern, default_match_plain, default_match_vim
|
||||
|
||||
local function apply_common_opts(line_to_cursor, opts)
|
||||
if opts and opts.max_len then
|
||||
return line_to_cursor:sub(#line_to_cursor - opts.max_len + 1)
|
||||
else
|
||||
return line_to_cursor
|
||||
end
|
||||
end
|
||||
|
||||
-- these functions get the line up to the cursor, the trigger, and then
|
||||
-- determine whether the trigger matches the current line.
|
||||
-- If the trigger does not match, the functions shall return nil, otherwise
|
||||
-- the matching substring and the list of captures (empty table if there aren't
|
||||
-- any).
|
||||
|
||||
local function match_plain(_, opts)
|
||||
if opts == nil then
|
||||
return default_match_plain
|
||||
end
|
||||
|
||||
return function(line_to_cursor, trigger)
|
||||
line_to_cursor = apply_common_opts(line_to_cursor, opts)
|
||||
if
|
||||
line_to_cursor:sub(#line_to_cursor - #trigger + 1, #line_to_cursor)
|
||||
== trigger
|
||||
then
|
||||
-- no captures for plain trigger.
|
||||
return trigger, {}
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
default_match_plain = match_plain(nil, {})
|
||||
|
||||
local function match_pattern(_, opts)
|
||||
if opts == nil then
|
||||
return default_match_pattern
|
||||
end
|
||||
|
||||
return function(line_to_cursor, trigger)
|
||||
line_to_cursor = apply_common_opts(line_to_cursor, opts)
|
||||
|
||||
-- look for match which ends at the cursor.
|
||||
-- put all results into a list, there might be many capture-groups.
|
||||
local find_res = { line_to_cursor:find(trigger .. "$") }
|
||||
|
||||
if #find_res > 0 then
|
||||
-- if there is a match, determine matching string, and the
|
||||
-- capture-groups.
|
||||
local captures = {}
|
||||
-- find_res[1] is `from`, find_res[2] is `to` (which we already know
|
||||
-- anyway).
|
||||
local from = find_res[1]
|
||||
local match = line_to_cursor:sub(from, #line_to_cursor)
|
||||
-- collect capture-groups.
|
||||
for i = 3, #find_res do
|
||||
captures[i - 2] = find_res[i]
|
||||
end
|
||||
return match, captures
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
default_match_pattern = match_pattern(nil, {})
|
||||
|
||||
local ecma_engine
|
||||
if jsregexp_compile_safe then
|
||||
ecma_engine = function(trig, opts)
|
||||
local trig_compiled, err_maybe = jsregexp_compile_safe(trig .. "$", "")
|
||||
if not trig_compiled then
|
||||
error(("Error while compiling regex: %s"):format(err_maybe))
|
||||
end
|
||||
|
||||
return function(line_to_cursor, _)
|
||||
line_to_cursor = apply_common_opts(line_to_cursor, opts)
|
||||
|
||||
-- get first (very likely only, since we appended the "$") match.
|
||||
local match = trig_compiled(line_to_cursor)[1]
|
||||
if match then
|
||||
-- return full match, and all groups.
|
||||
return line_to_cursor:sub(match.begin_ind), match.groups
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
ecma_engine = function(x, opts)
|
||||
return match_plain(x, opts)
|
||||
end
|
||||
end
|
||||
|
||||
local function match_vim(_, opts)
|
||||
if opts == nil then
|
||||
return default_match_vim
|
||||
end
|
||||
|
||||
return function(line_to_cursor, trigger)
|
||||
line_to_cursor = apply_common_opts(line_to_cursor, opts)
|
||||
|
||||
local matchlist = vim.fn.matchlist(line_to_cursor, trigger .. "$")
|
||||
if #matchlist > 0 then
|
||||
local groups = {}
|
||||
for i = 2, 10 do
|
||||
-- PROBLEM: vim does not differentiate between an empty ("")
|
||||
-- and a missing capture.
|
||||
-- Since we need to differentiate between the two (Check `:h
|
||||
-- luasnip-variables-lsp-variables`), we assume, here, that an
|
||||
-- empty string is an unmatched group.
|
||||
groups[i - 1] = matchlist[i] ~= "" and matchlist[i] or nil
|
||||
end
|
||||
return matchlist[1], groups
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
default_match_vim = match_vim(nil, {})
|
||||
|
||||
return {
|
||||
plain = match_plain,
|
||||
pattern = match_pattern,
|
||||
ecma = ecma_engine,
|
||||
vim = match_vim,
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
local snippet_collection = require("luasnip.session.snippet_collection")
|
||||
|
||||
local M = {}
|
||||
|
||||
local refresh_enqueued = false
|
||||
local next_refresh_fts = {}
|
||||
|
||||
function M.refresh_notify(ft)
|
||||
next_refresh_fts[ft] = true
|
||||
|
||||
if not refresh_enqueued then
|
||||
vim.schedule(function()
|
||||
for enq_ft, _ in pairs(next_refresh_fts) do
|
||||
snippet_collection.refresh_notify(enq_ft)
|
||||
end
|
||||
|
||||
next_refresh_fts = {}
|
||||
refresh_enqueued = false
|
||||
end)
|
||||
|
||||
refresh_enqueued = true
|
||||
end
|
||||
end
|
||||
|
||||
local clean_enqueued = false
|
||||
function M.clean_invalidated()
|
||||
if not clean_enqueued then
|
||||
vim.schedule(function()
|
||||
snippet_collection.clean_invalidated({ inv_limit = 100 })
|
||||
end)
|
||||
end
|
||||
clean_enqueued = true
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,52 @@
|
||||
-- used to store values like current nodes or the active node for autocommands.
|
||||
local M = {}
|
||||
|
||||
M.ft_redirect = {}
|
||||
setmetatable(M.ft_redirect, {
|
||||
__index = function(table, key)
|
||||
-- no entry for this ft(key), set it to avoid calls on each expand for
|
||||
-- this filetype.
|
||||
local val = { key }
|
||||
rawset(table, key, val)
|
||||
return val
|
||||
end,
|
||||
})
|
||||
|
||||
M.current_nodes = {}
|
||||
-- roots of snippet-trees, per-buffer.
|
||||
-- snippet_roots[n] => list of snippet-roots in buffer n.
|
||||
M.snippet_roots = setmetatable({}, {
|
||||
-- create missing lists automatically.
|
||||
__index = function(t, k)
|
||||
local new_t = {}
|
||||
rawset(t, k, new_t)
|
||||
return new_t
|
||||
end,
|
||||
})
|
||||
M.ns_id = vim.api.nvim_create_namespace("Luasnip")
|
||||
M.active_choice_nodes = {}
|
||||
|
||||
-- only here for overview.
|
||||
M.latest_load_ft = nil
|
||||
|
||||
M.last_expand_snip = nil
|
||||
M.last_expand_opts = nil
|
||||
|
||||
-- jump_active is set while luasnip moves the cursor, prevents
|
||||
-- (for example) updating dependents or deleting a snippet via
|
||||
-- exit_out_of_region while jumping.
|
||||
-- init with false, it will be set by (eg.) ls.jump().
|
||||
M.jump_active = false
|
||||
|
||||
-- initial value, might be overwritten immediately.
|
||||
-- No danger of overwriting user-config, since this has to be loaded to allow
|
||||
-- overwriting.
|
||||
M.config = require("luasnip.default_config")
|
||||
|
||||
M.loaded_fts = {}
|
||||
|
||||
function M.get_snip_env()
|
||||
return M.config.snip_env
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,328 @@
|
||||
local source = require("luasnip.session.snippet_collection.source")
|
||||
local u_table = require("luasnip.util.table")
|
||||
local auto_creating_tables =
|
||||
require("luasnip.util.auto_table").warn_depth_autotable
|
||||
local session = require("luasnip.session")
|
||||
|
||||
-- store snippets by some key.
|
||||
-- also ordered by filetype, eg.
|
||||
-- {
|
||||
-- key = {
|
||||
-- ft1 = {...},
|
||||
-- ft2 = {...}
|
||||
-- }
|
||||
-- }
|
||||
local M = {
|
||||
invalidated_count = 0,
|
||||
}
|
||||
|
||||
local by_key = {}
|
||||
|
||||
-- stores snippets/autosnippets by priority.
|
||||
local by_prio = {
|
||||
snippets = {
|
||||
-- stores sorted keys, eg 1=1000, 2=1010, 3=1020,..., used for
|
||||
-- quick iterating.
|
||||
order = {},
|
||||
},
|
||||
autosnippets = {
|
||||
order = {},
|
||||
},
|
||||
}
|
||||
|
||||
-- this isn't in util/util.lua due to circular dependencies. Would be cleaner
|
||||
-- to include it there, but it's alright to keep here for now.
|
||||
--
|
||||
-- this is linear, binary search would certainly be nicer, but for our
|
||||
-- applications this should easily be enough.
|
||||
local function insert_sorted_unique(t, k)
|
||||
local tbl_len = #t
|
||||
|
||||
local i = 1
|
||||
-- k does not yet exist in table, find first i so t[i] > k.
|
||||
for _ = 1, tbl_len do
|
||||
if t[i] > k then
|
||||
break
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
-- shift all t[j] with j > i back by one.
|
||||
for j = tbl_len, i, -1 do
|
||||
t[j + 1] = t[j]
|
||||
end
|
||||
|
||||
t[i] = k
|
||||
end
|
||||
|
||||
local by_prio_snippets_mt = {
|
||||
__index = function(s, k)
|
||||
-- make new tables as they are indexed
|
||||
return auto_creating_tables(s, k, 3)
|
||||
end,
|
||||
__newindex = function(t, k, v)
|
||||
-- update priority-order as well.
|
||||
insert_sorted_unique(t.order, k)
|
||||
rawset(t, k, v)
|
||||
end,
|
||||
}
|
||||
-- metatable for the by_prio table used when by_prio.type[prio] is reset
|
||||
-- create here so that it can be shared and only has to be created once
|
||||
local prio_mt2 = {
|
||||
__index = function(s, k)
|
||||
-- make new tables as they are indexed
|
||||
return auto_creating_tables(s, k, 2)
|
||||
end,
|
||||
}
|
||||
|
||||
setmetatable(by_prio.snippets, by_prio_snippets_mt)
|
||||
setmetatable(by_prio.autosnippets, by_prio_snippets_mt)
|
||||
|
||||
-- iterate priorities, high to low.
|
||||
local function prio_iter(type)
|
||||
local order = by_prio[type].order
|
||||
local i = #order + 1
|
||||
|
||||
return function()
|
||||
i = i - 1
|
||||
if i > 0 then
|
||||
return by_prio[type][order[i]]
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local by_ft = {
|
||||
snippets = {},
|
||||
autosnippets = {},
|
||||
}
|
||||
|
||||
local by_ft_snippets_mt = {
|
||||
__index = function(s, k)
|
||||
return auto_creating_tables(s, k, 2)
|
||||
end,
|
||||
}
|
||||
setmetatable(by_ft.snippets, by_ft_snippets_mt)
|
||||
setmetatable(by_ft.autosnippets, by_ft_snippets_mt)
|
||||
|
||||
local by_id = setmetatable({}, {
|
||||
-- make by_id-table weak (v).
|
||||
-- this means it won't be necessary to explicitly nil values (snippets) in
|
||||
-- this table.
|
||||
__mode = "v",
|
||||
})
|
||||
|
||||
-- ft: any filetype, optional.
|
||||
function M.clear_snippets(ft)
|
||||
if ft then
|
||||
-- remove all ft-(auto)snippets for all priorities.
|
||||
-- set to empty table so we won't need to rebuild/clear the order-table.
|
||||
for _, prio in ipairs(by_prio.snippets.order) do
|
||||
by_prio.snippets[prio][ft] = {}
|
||||
end
|
||||
for _, prio in ipairs(by_prio.autosnippets.order) do
|
||||
by_prio.autosnippets[prio][ft] = {}
|
||||
end
|
||||
|
||||
by_ft.snippets[ft] = nil
|
||||
by_ft.autosnippets[ft] = nil
|
||||
|
||||
for key, _ in pairs(by_key) do
|
||||
by_key[key][ft] = nil
|
||||
end
|
||||
else
|
||||
-- remove all (auto)snippets for all priorities.
|
||||
for _, prio in ipairs(by_prio.snippets.order) do
|
||||
by_prio.snippets[prio] = {}
|
||||
setmetatable(by_prio.snippets[prio], prio_mt2)
|
||||
end
|
||||
for _, prio in ipairs(by_prio.autosnippets.order) do
|
||||
by_prio.autosnippets[prio] = {}
|
||||
setmetatable(by_prio.autosnippets[prio], prio_mt2)
|
||||
end
|
||||
|
||||
by_key = {}
|
||||
by_ft.snippets = {}
|
||||
setmetatable(by_ft.snippets, by_ft_snippets_mt)
|
||||
by_ft.autosnippets = {}
|
||||
setmetatable(by_ft.autosnippets, by_ft_snippets_mt)
|
||||
end
|
||||
end
|
||||
|
||||
function M.match_snippet(line, fts, type)
|
||||
local expand_params
|
||||
|
||||
for prio_by_ft in prio_iter(type) do
|
||||
for _, ft in ipairs(fts) do
|
||||
for _, snip in ipairs(prio_by_ft[ft] or {}) do
|
||||
expand_params = snip:matches(line)
|
||||
if expand_params then
|
||||
-- return matching snippet and table with expand-parameters.
|
||||
return snip, expand_params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function without_invalidated(snippets_by_ft)
|
||||
local new_snippets = {}
|
||||
|
||||
for ft, ft_snippets in pairs(snippets_by_ft) do
|
||||
new_snippets[ft] = {}
|
||||
for _, snippet in ipairs(ft_snippets) do
|
||||
if not snippet.invalidated then
|
||||
table.insert(new_snippets[ft], snippet)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return new_snippets
|
||||
end
|
||||
|
||||
function M.clean_invalidated(opts)
|
||||
if opts.inv_limit then
|
||||
if M.invalidated_count <= opts.inv_limit then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- remove invalidated snippets from all tables.
|
||||
for _, type_snippets in pairs(by_prio) do
|
||||
for key, prio_snippets in pairs(type_snippets) do
|
||||
if key ~= "order" then
|
||||
type_snippets[key] = without_invalidated(prio_snippets)
|
||||
setmetatable(type_snippets[key], prio_mt2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for type, type_snippets in pairs(by_ft) do
|
||||
by_ft[type] = without_invalidated(type_snippets)
|
||||
setmetatable(by_ft[type], by_ft_snippets_mt)
|
||||
end
|
||||
|
||||
for key, key_snippets in pairs(by_key) do
|
||||
by_key[key] = without_invalidated(key_snippets)
|
||||
end
|
||||
|
||||
M.invalidated_count = 0
|
||||
end
|
||||
|
||||
local function invalidate_addables(addables_by_ft)
|
||||
for _, addables in pairs(addables_by_ft) do
|
||||
for _, addable in ipairs(addables) do
|
||||
for _, expandable in ipairs(addable:retrieve_all()) do
|
||||
expandable:invalidate()
|
||||
end
|
||||
end
|
||||
end
|
||||
M.clean_invalidated({ inv_limit = 100 })
|
||||
end
|
||||
|
||||
local current_id = 0
|
||||
-- snippets like {ft1={<snippets>}, ft2={<snippets>}}, opts should be properly
|
||||
-- initialized with default values.
|
||||
function M.add_snippets(snippets, opts)
|
||||
for ft, ft_snippets in pairs(snippets) do
|
||||
for _, addable in ipairs(ft_snippets) do
|
||||
for _, snip in ipairs(addable:retrieve_all()) do
|
||||
local snip_prio = opts.override_priority
|
||||
or (snip.priority and snip.priority)
|
||||
or opts.default_priority
|
||||
or 1000
|
||||
|
||||
-- if snippetType undefined by snippet, take default value from opts
|
||||
local snip_type = snip.snippetType ~= nil and snip.snippetType
|
||||
or opts.type
|
||||
assert(
|
||||
snip_type == "autosnippets" or snip_type == "snippets",
|
||||
"snippetType must be either 'autosnippets' or 'snippets', was "
|
||||
.. vim.inspect(snip_type)
|
||||
)
|
||||
|
||||
local snip_ft = snip.filetype or ft
|
||||
|
||||
snip.id = current_id
|
||||
current_id = current_id + 1
|
||||
|
||||
-- do the insertion
|
||||
table.insert(by_prio[snip_type][snip_prio][snip_ft], snip)
|
||||
table.insert(by_ft[snip_type][snip_ft], snip)
|
||||
by_id[snip.id] = snip
|
||||
|
||||
-- set source if it is available.
|
||||
if snip._source then
|
||||
source.set(snip, snip._source)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if opts.key then
|
||||
if by_key[opts.key] then
|
||||
invalidate_addables(by_key[opts.key])
|
||||
end
|
||||
by_key[opts.key] = snippets
|
||||
end
|
||||
end
|
||||
|
||||
-- specialized copy functions to not loose performance on ifs when copying
|
||||
-- and to be able to specify when pairs or ipairs is used
|
||||
local function copy_by_ft_type_ft(tab)
|
||||
local r = {}
|
||||
for k, v in ipairs(tab) do
|
||||
r[k] = v
|
||||
end
|
||||
return r
|
||||
end
|
||||
local function copy_by_ft_type(tab)
|
||||
local r = {}
|
||||
for k, v in pairs(tab) do
|
||||
r[k] = copy_by_ft_type_ft(v)
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
-- ft may be nil, type not.
|
||||
function M.get_snippets(ft, type)
|
||||
if ft then
|
||||
return copy_by_ft_type_ft(by_ft[type][ft])
|
||||
else
|
||||
return copy_by_ft_type(by_ft[type])
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_id_snippet(id)
|
||||
return by_id[id]
|
||||
end
|
||||
|
||||
local function get_all_snippet_fts()
|
||||
local ft_set = {}
|
||||
for ft, _ in pairs(by_ft.snippets) do
|
||||
ft_set[ft] = true
|
||||
end
|
||||
for ft, _ in pairs(by_ft.autosnippets) do
|
||||
ft_set[ft] = true
|
||||
end
|
||||
|
||||
return u_table.set_to_list(ft_set)
|
||||
end
|
||||
|
||||
-- modules that want to call refresh_notify probably also want to notify others
|
||||
-- of adding those snippets => put those functions into the same module.
|
||||
function M.refresh_notify(ft_or_nil)
|
||||
local fts = ft_or_nil and { ft_or_nil } or get_all_snippet_fts()
|
||||
|
||||
for _, ft in ipairs(fts) do
|
||||
session.latest_load_ft = ft
|
||||
vim.api.nvim_exec_autocmds(
|
||||
"User",
|
||||
{ pattern = "LuasnipSnippetsAdded", modeline = false }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,38 @@
|
||||
local id_to_source = {}
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.from_debuginfo(debuginfo)
|
||||
assert(debuginfo.source, "debuginfo contains source")
|
||||
assert(
|
||||
debuginfo.source:match("^@"),
|
||||
"debuginfo-source is a file: " .. debuginfo.source
|
||||
)
|
||||
|
||||
return {
|
||||
-- omit leading '@'.
|
||||
file = debuginfo.source:sub(2),
|
||||
line = debuginfo.currentline,
|
||||
}
|
||||
end
|
||||
|
||||
function M.from_location(file, opts)
|
||||
assert(file, "source needs file")
|
||||
opts = opts or {}
|
||||
|
||||
return { file = file, line = opts.line, line_end = opts.line_end }
|
||||
end
|
||||
|
||||
function M.set(snippet, source)
|
||||
-- snippets only get their id after being added, make sure this is the
|
||||
-- case.
|
||||
assert(snippet.id, "snippet has an id")
|
||||
|
||||
id_to_source[snippet.id] = source
|
||||
end
|
||||
|
||||
function M.get(snippet)
|
||||
return id_to_source[snippet.id]
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,208 @@
|
||||
local util = require("luasnip.util.util")
|
||||
local select_util = require("luasnip.util.select")
|
||||
local time_util = require("luasnip.util.time")
|
||||
local lazy_vars = {}
|
||||
|
||||
-- Variables defined in https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables
|
||||
|
||||
-- Inherited from TextMate
|
||||
function lazy_vars.TM_FILENAME()
|
||||
return vim.fn.expand("%:t")
|
||||
end
|
||||
|
||||
function lazy_vars.TM_FILENAME_BASE()
|
||||
return vim.fn.expand("%:t:s?\\.[^\\.]\\+$??")
|
||||
end
|
||||
|
||||
function lazy_vars.TM_DIRECTORY()
|
||||
return vim.fn.expand("%:p:h")
|
||||
end
|
||||
|
||||
function lazy_vars.TM_FILEPATH()
|
||||
return vim.fn.expand("%:p")
|
||||
end
|
||||
|
||||
-- Vscode only
|
||||
|
||||
function lazy_vars.CLIPBOARD() -- The contents of your clipboard
|
||||
return vim.fn.getreg('"', 1, true)
|
||||
end
|
||||
|
||||
local function buf_to_ws_part()
|
||||
local LSP_WORSKPACE_PARTS = "LSP_WORSKPACE_PARTS"
|
||||
local ok, ws_parts = pcall(vim.api.nvim_buf_get_var, 0, LSP_WORSKPACE_PARTS)
|
||||
if not ok then
|
||||
local file_path = vim.fn.expand("%:p")
|
||||
|
||||
for _, ws in pairs(vim.lsp.buf.list_workspace_folders()) do
|
||||
if file_path:find(ws, 1, true) == 1 then
|
||||
ws_parts = { ws, file_path:sub(#ws + 2, -1) }
|
||||
break
|
||||
end
|
||||
end
|
||||
-- If it can't be extracted from lsp, then we use the file path
|
||||
if not ok and not ws_parts then
|
||||
ws_parts = { vim.fn.expand("%:p:h"), vim.fn.expand("%:p:t") }
|
||||
end
|
||||
vim.api.nvim_buf_set_var(0, LSP_WORSKPACE_PARTS, ws_parts)
|
||||
end
|
||||
return ws_parts
|
||||
end
|
||||
|
||||
function lazy_vars.RELATIVE_FILEPATH() -- The relative (to the opened workspace or folder) file path of the current document
|
||||
return buf_to_ws_part()[2]
|
||||
end
|
||||
|
||||
function lazy_vars.WORKSPACE_FOLDER() -- The path of the opened workspace or folder
|
||||
return buf_to_ws_part()[1]
|
||||
end
|
||||
|
||||
function lazy_vars.WORKSPACE_NAME() -- The name of the opened workspace or folder
|
||||
local parts = vim.split(buf_to_ws_part()[1] or "", "[\\/]")
|
||||
return parts[#parts]
|
||||
end
|
||||
|
||||
-- DateTime Related
|
||||
function lazy_vars.CURRENT_YEAR()
|
||||
return os.date("%Y")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_YEAR_SHORT()
|
||||
return os.date("%y")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_MONTH()
|
||||
return os.date("%m")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_MONTH_NAME()
|
||||
return os.date("%B")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_MONTH_NAME_SHORT()
|
||||
return os.date("%b")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_DATE()
|
||||
return os.date("%d")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_DAY_NAME()
|
||||
return os.date("%A")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_DAY_NAME_SHORT()
|
||||
return os.date("%a")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_HOUR()
|
||||
return os.date("%H")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_MINUTE()
|
||||
return os.date("%M")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_SECOND()
|
||||
return os.date("%S")
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_SECONDS_UNIX()
|
||||
return tostring(os.time())
|
||||
end
|
||||
|
||||
function lazy_vars.CURRENT_TIMEZONE_OFFSET()
|
||||
return time_util
|
||||
.get_timezone_offset(os.time())
|
||||
:gsub("([+-])(%d%d)(%d%d)$", "%1%2:%3")
|
||||
end
|
||||
|
||||
-- For inserting random values
|
||||
|
||||
math.randomseed(os.time())
|
||||
|
||||
function lazy_vars.RANDOM()
|
||||
return string.format("%06d", math.random(999999))
|
||||
end
|
||||
|
||||
function lazy_vars.RANDOM_HEX()
|
||||
return string.format("%06x", math.random(16777216)) --16^6
|
||||
end
|
||||
|
||||
function lazy_vars.UUID()
|
||||
local random = math.random
|
||||
local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
|
||||
local out
|
||||
local function subs(c)
|
||||
local v = (((c == "x") and random(0, 15)) or random(8, 11))
|
||||
return string.format("%x", v)
|
||||
end
|
||||
|
||||
out = template:gsub("[xy]", subs)
|
||||
return out
|
||||
end
|
||||
|
||||
function lazy_vars.LINE_COMMENT()
|
||||
return util.buffer_comment_chars()[1]
|
||||
end
|
||||
|
||||
function lazy_vars.BLOCK_COMMENT_START()
|
||||
return util.buffer_comment_chars()[2]
|
||||
end
|
||||
|
||||
function lazy_vars.BLOCK_COMMENT_END()
|
||||
return util.buffer_comment_chars()[3]
|
||||
end
|
||||
|
||||
-- These are the vars that have to be populated once the snippet starts to avoid any issue
|
||||
local function eager_vars(info)
|
||||
local vars = {}
|
||||
local pos = info.pos
|
||||
vars.TM_CURRENT_LINE =
|
||||
vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1]
|
||||
vars.TM_CURRENT_WORD = util.word_under_cursor(pos, vars.TM_CURRENT_LINE)
|
||||
vars.TM_LINE_INDEX = tostring(pos[1])
|
||||
vars.TM_LINE_NUMBER = tostring(pos[1] + 1)
|
||||
vars.LS_SELECT_RAW, vars.LS_SELECT_DEDENT, vars.TM_SELECTED_TEXT =
|
||||
select_util.retrieve()
|
||||
-- These are for backward compatibility, for now on all builtins that are not part of TM_ go in LS_
|
||||
vars.SELECT_RAW, vars.SELECT_DEDENT =
|
||||
vars.LS_SELECT_RAW, vars.LS_SELECT_DEDENT
|
||||
for i, cap in ipairs(info.captures) do
|
||||
vars["LS_CAPTURE_" .. i] = cap
|
||||
end
|
||||
vars.LS_TRIGGER = info.trigger
|
||||
return vars
|
||||
end
|
||||
|
||||
local builtin_ns = { SELECT = true, LS = true }
|
||||
|
||||
for name, _ in pairs(lazy_vars) do
|
||||
local parts = vim.split(name, "_")
|
||||
if #parts > 1 then
|
||||
builtin_ns[parts[1]] = true
|
||||
end
|
||||
end
|
||||
|
||||
local _is_table = {
|
||||
TM_SELECTED_TEXT = true,
|
||||
SELECT_RAW = true,
|
||||
SELECT_DEDENT = true,
|
||||
}
|
||||
|
||||
return {
|
||||
is_table = function(key)
|
||||
-- variables generated at runtime by treesitter-postfix.
|
||||
if
|
||||
key:match("LS_TSCAPTURE_.*")
|
||||
or key == "LS_TSPOSTFIX_MATCH"
|
||||
or key == "LS_TSPOSTFIX_DATA"
|
||||
then
|
||||
return true
|
||||
end
|
||||
return _is_table[key] or false
|
||||
end,
|
||||
vars = lazy_vars,
|
||||
init = eager_vars,
|
||||
builtin_ns = builtin_ns,
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
-- depth specifies how many levels under this table should be allowed to index
|
||||
-- throug this metamethod
|
||||
-- set depth to 0 to disable checking
|
||||
-- Acknowledgment: This is (maybe more than) inspired by
|
||||
-- https://lua-users.org/wiki/AutomagicTables so special thanks to
|
||||
-- Thomas Wrensch and Rici Lake for sharing their ideas on this topic.
|
||||
local function auto_creating_tables_warn_depth(self, key, depth)
|
||||
local t = {}
|
||||
assert(depth ~= 1, "don't index at that level")
|
||||
setmetatable(t, {
|
||||
-- creating a new function on each time (could be shared) isn't that
|
||||
-- nice. Nonetheless this shouldn't be too bad, as these are only
|
||||
-- created twice (auto+snippet) per ft and twice for each prio,ft
|
||||
-- combination
|
||||
__index = function(s, k)
|
||||
return auto_creating_tables_warn_depth(s, k, depth - 1)
|
||||
end,
|
||||
})
|
||||
self[key] = t
|
||||
return t
|
||||
end
|
||||
|
||||
local function auto_creating_tables(self, key, depth)
|
||||
local t = {}
|
||||
if depth ~= 1 then
|
||||
setmetatable(t, {
|
||||
__index = function(s, k)
|
||||
return auto_creating_tables(s, k, depth - 1)
|
||||
end,
|
||||
})
|
||||
end
|
||||
self[key] = t
|
||||
return t
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.autotable(max_depth, opts)
|
||||
opts = opts or {}
|
||||
local warn = vim.F.if_nil(opts.warn, false)
|
||||
|
||||
local auto_table_func = warn and auto_creating_tables_warn_depth
|
||||
or auto_creating_tables
|
||||
|
||||
return setmetatable({}, {
|
||||
__index = function(s, k)
|
||||
return auto_table_func(s, k, max_depth - 1)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
M.warn_depth_autotable = auto_creating_tables_warn_depth
|
||||
|
||||
return M
|
||||
@ -0,0 +1,63 @@
|
||||
local Dictionary = {}
|
||||
|
||||
local function new(o)
|
||||
return setmetatable(o or {}, {
|
||||
__index = Dictionary,
|
||||
})
|
||||
end
|
||||
|
||||
function Dictionary:set(path, value)
|
||||
-- Insp(path)
|
||||
-- print("val: ", value)
|
||||
local current_table = self
|
||||
for i = 1, #path - 1 do
|
||||
local crt_key = path[i]
|
||||
if not current_table[crt_key] then
|
||||
current_table[crt_key] = {}
|
||||
end
|
||||
current_table = current_table[crt_key]
|
||||
end
|
||||
current_table[path[#path]] = value
|
||||
end
|
||||
|
||||
function Dictionary:get(path)
|
||||
local current_table = self
|
||||
for _, v in ipairs(path) do
|
||||
if not current_table[v] then
|
||||
return nil
|
||||
end
|
||||
current_table = current_table[v]
|
||||
end
|
||||
-- may not be a table.
|
||||
return current_table
|
||||
end
|
||||
|
||||
function Dictionary:find_all(path, key)
|
||||
local res = {}
|
||||
local to_search = { self:get(path) }
|
||||
if not to_search[1] then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- weird hybrid of depth- and breadth-first search for key, collect values in res.
|
||||
local search_index = 1
|
||||
local search_size = 1
|
||||
while search_size > 0 do
|
||||
for k, v in pairs(to_search[search_index]) do
|
||||
if k == key then
|
||||
res[#res + 1] = v
|
||||
else
|
||||
to_search[search_index + search_size] = v
|
||||
search_size = search_size + 1
|
||||
end
|
||||
end
|
||||
search_index = search_index + 1
|
||||
search_size = search_size - 1
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
return {
|
||||
new = new,
|
||||
}
|
||||
@ -0,0 +1,263 @@
|
||||
local tbl_util = require("luasnip.util.table")
|
||||
local autotable = require("luasnip.util.auto_table").autotable
|
||||
|
||||
local DirectedGraph = {}
|
||||
|
||||
-- set __index directly in DirectedGraph, otherwise each DirectedGraph-object would have its'
|
||||
-- own metatable (one more table around), which would not be bad, but
|
||||
-- unnecessary nonetheless.
|
||||
DirectedGraph.__index = DirectedGraph
|
||||
|
||||
local Vertex = {}
|
||||
Vertex.__index = Vertex
|
||||
|
||||
local function new_graph()
|
||||
return setmetatable({
|
||||
-- all vertices of this graph.
|
||||
vertices = {},
|
||||
}, DirectedGraph)
|
||||
end
|
||||
local function new_vertex()
|
||||
return setmetatable({
|
||||
-- vertices this vertex has an edge from/to.
|
||||
-- map[vert -> bool]
|
||||
incoming_edge_verts = {},
|
||||
outgoing_edge_verts = {},
|
||||
}, Vertex)
|
||||
end
|
||||
|
||||
---Add new vertex to the DirectedGraph
|
||||
---@return table: the generated vertex, to be used in `set_edge`, for example.
|
||||
function DirectedGraph:add_vertex()
|
||||
local vert = new_vertex()
|
||||
table.insert(self.vertices, vert)
|
||||
return vert
|
||||
end
|
||||
|
||||
---Remove vertex and its edges from DirectedGraph.
|
||||
---@param v table: the vertex.
|
||||
function DirectedGraph:clear_vertex(v)
|
||||
if not vim.tbl_contains(self.vertices, v) then
|
||||
-- vertex does not belong to this graph. Maybe throw error/make
|
||||
-- condition known?
|
||||
return
|
||||
end
|
||||
-- remove outgoing..
|
||||
for outgoing_edge_vert, _ in pairs(v.outgoing_edge_verts) do
|
||||
self:clear_edge(v, outgoing_edge_vert)
|
||||
end
|
||||
-- ..and incoming edges with v from the graph.
|
||||
for incoming_edge_vert, _ in pairs(v.incoming_edge_verts) do
|
||||
self:clear_edge(incoming_edge_vert, v)
|
||||
end
|
||||
end
|
||||
|
||||
---Add edge from v1 to v2
|
||||
---@param v1 table: vertex in the graph.
|
||||
---@param v2 table: vertex in the graph.
|
||||
function DirectedGraph:set_edge(v1, v2)
|
||||
if v1.outgoing_edge_verts[v2] then
|
||||
-- the edge already exists. Don't return an error, for now.
|
||||
return
|
||||
end
|
||||
-- link vertices.
|
||||
v1.outgoing_edge_verts[v2] = true
|
||||
v2.incoming_edge_verts[v1] = true
|
||||
end
|
||||
|
||||
---Remove edge from v1 to v2
|
||||
---@param v1 table: vertex in the graph.
|
||||
---@param v2 table: vertex in the graph.
|
||||
function DirectedGraph:clear_edge(v1, v2)
|
||||
assert(v1.outgoing_edge_verts[v2], "nonexistent edge cannot be removed.")
|
||||
-- unlink vertices.
|
||||
v1.outgoing_edge_verts[v2] = nil
|
||||
v2.incoming_edge_verts[v1] = nil
|
||||
end
|
||||
|
||||
---Find and return verts with indegree 0.
|
||||
---@param graph table: graph.
|
||||
---@return table of vertices.
|
||||
local function source_verts(graph)
|
||||
local indegree_0_verts = {}
|
||||
for _, vert in ipairs(graph.vertices) do
|
||||
if vim.tbl_count(vert.incoming_edge_verts) == 0 then
|
||||
table.insert(indegree_0_verts, vert)
|
||||
end
|
||||
end
|
||||
return indegree_0_verts
|
||||
end
|
||||
|
||||
---Copy graph.
|
||||
---@param graph table: graph.
|
||||
---@return table,table: copied graph and table for mapping copied node to
|
||||
---original node(original_vert[some_vert_from_copy] -> corresponding original
|
||||
---vert).
|
||||
local function graph_copy(graph)
|
||||
local copy = vim.deepcopy(graph)
|
||||
local original_vert = {}
|
||||
for i, copy_vert in ipairs(copy.vertices) do
|
||||
original_vert[copy_vert] = graph.vertices[i]
|
||||
end
|
||||
return copy, original_vert
|
||||
end
|
||||
|
||||
---Generate a (it's not necessarily unique) topological sorting of this graphs
|
||||
---vertices.
|
||||
---https://en.wikipedia.org/wiki/Topological_sorting, this uses Kahn's Algorithm.
|
||||
---@return table|nil: sorted vertices of this graph, nil if there is no
|
||||
---topological sorting (eg. if the graph has a cycle).
|
||||
function DirectedGraph:topological_sort()
|
||||
local sorting = {}
|
||||
|
||||
-- copy self so edges can be removed without affecting the real graph.
|
||||
local graph, original_vert = graph_copy(self)
|
||||
|
||||
-- find vertices without incoming edges.
|
||||
-- invariant: at the end of each step, sources contains all vertices
|
||||
-- without incoming edges.
|
||||
local sources = source_verts(graph)
|
||||
while #sources > 0 do
|
||||
-- pop v from sources.
|
||||
local v = sources[#sources]
|
||||
sources[#sources] = nil
|
||||
|
||||
-- v has no incoming edges, it can be next in the sorting.
|
||||
-- important!! don't insert v, insert the corresponding vertex from the
|
||||
-- original graph. The copied vertices are not known outside this
|
||||
-- function (alternative: maybe return indices in graph.vertices?).
|
||||
table.insert(sorting, original_vert[v])
|
||||
|
||||
-- find vertices which, if v is removed from graph, have no more incoming edges.
|
||||
-- Those are sources after v is removed.
|
||||
for outgoing_edge_vert, _ in pairs(v.outgoing_edge_verts) do
|
||||
-- there is one edge, it has to be from v.
|
||||
if vim.tbl_count(outgoing_edge_vert.incoming_edge_verts) == 1 then
|
||||
table.insert(sources, outgoing_edge_vert)
|
||||
end
|
||||
end
|
||||
|
||||
-- finally: remove v from graph and sources.
|
||||
graph:clear_vertex(v)
|
||||
end
|
||||
|
||||
if #sorting ~= #self.vertices then
|
||||
-- error: the sorting does not contain all vertices -> the graph has a cycle.
|
||||
return nil
|
||||
end
|
||||
return sorting
|
||||
end
|
||||
|
||||
-- return all vertices reachable from this one.
|
||||
function DirectedGraph:connected_component(vert, edge_direction)
|
||||
local outgoing_vertices_field = edge_direction == "Backward"
|
||||
and "incoming_edge_verts"
|
||||
or "outgoing_edge_verts"
|
||||
|
||||
local visited = {}
|
||||
local to_visit = { [vert] = true }
|
||||
|
||||
-- get any value in table.
|
||||
local next_vert, _ = next(to_visit, nil)
|
||||
while next_vert do
|
||||
to_visit[next_vert] = nil
|
||||
visited[next_vert] = true
|
||||
|
||||
for neighbor, _ in pairs(next_vert[outgoing_vertices_field]) do
|
||||
if not visited[neighbor] then
|
||||
to_visit[neighbor] = true
|
||||
end
|
||||
end
|
||||
|
||||
next_vert, _ = next(to_visit, nil)
|
||||
end
|
||||
|
||||
return tbl_util.set_to_list(visited)
|
||||
end
|
||||
|
||||
-- Very useful to have a graph where vertices are associated with some label.
|
||||
-- This just proxies DirectedGraph and swaps labels and vertices in
|
||||
-- parameters/return-values.
|
||||
local LabeledDigraph = {}
|
||||
LabeledDigraph.__index = LabeledDigraph
|
||||
|
||||
local function new_labeled_graph()
|
||||
return setmetatable({
|
||||
graph = new_graph(),
|
||||
label_to_vert = {},
|
||||
vert_to_label = {},
|
||||
-- map label -> origin-vert -> dest-vert
|
||||
label_to_verts = autotable(3, { warn = false }),
|
||||
-- map edge (origin,dest) to set of labels.
|
||||
verts_to_label = autotable(3, { warn = false }),
|
||||
}, LabeledDigraph)
|
||||
end
|
||||
|
||||
function LabeledDigraph:set_vertex(label)
|
||||
if self.label_to_vert[label] then
|
||||
-- don't add same label again.
|
||||
return
|
||||
end
|
||||
|
||||
local vert = self.graph:add_vertex()
|
||||
self.label_to_vert[label] = vert
|
||||
self.vert_to_label[vert] = label
|
||||
end
|
||||
|
||||
function LabeledDigraph:set_edge(lv1, lv2, edge_label)
|
||||
-- ensure vertices exist.
|
||||
self:set_vertex(lv1)
|
||||
self:set_vertex(lv2)
|
||||
|
||||
if self.verts_to_label[lv1][lv2][edge_label] then
|
||||
-- edge exists, do nothing.
|
||||
return
|
||||
end
|
||||
|
||||
-- determine before setting the lv1-lv2-edge.
|
||||
local other_edge_exists = next(self.verts_to_label[lv1][lv2], nil) ~= nil
|
||||
|
||||
-- store both associations;
|
||||
self.verts_to_label[lv1][lv2][edge_label] = true
|
||||
self.label_to_verts[edge_label][lv1][lv2] = true
|
||||
|
||||
if other_edge_exists then
|
||||
-- there already exists an entry for this edge, no need to add it to
|
||||
-- the graph.
|
||||
return
|
||||
end
|
||||
self.graph:set_edge(self.label_to_vert[lv1], self.label_to_vert[lv2])
|
||||
end
|
||||
|
||||
function LabeledDigraph:clear_edge(lv1, lv2, ledge)
|
||||
if not self.verts_to_label[lv1][lv2][ledge] then
|
||||
-- edge does not exist, do nothing.
|
||||
return
|
||||
end
|
||||
|
||||
self.verts_to_label[lv1][lv2][ledge] = nil
|
||||
if next(self.verts_to_label[lv1][lv2]) == nil then
|
||||
-- removed last edge between v1, v2 -> remove edge in graph.
|
||||
self.graph:clear_edge(self.label_to_vert[lv1], self.label_to_vert[lv2])
|
||||
end
|
||||
end
|
||||
function LabeledDigraph:clear_edges(label)
|
||||
for lv1, lv2 in pairs(self.label_to_verts[label]) do
|
||||
self:clear_edge(lv1, lv2, label)
|
||||
end
|
||||
-- set to nil, not {}, so autotable can work its magic.
|
||||
self.label_to_verts[label] = nil
|
||||
end
|
||||
|
||||
function LabeledDigraph:connected_component(lv, edge_direction)
|
||||
self:set_vertex(lv)
|
||||
|
||||
return vim.tbl_map(function(v)
|
||||
return self.vert_to_label[v]
|
||||
end, self.graph:connected_component(self.label_to_vert[lv], edge_direction))
|
||||
end
|
||||
|
||||
return {
|
||||
new = new_graph,
|
||||
new_labeled = new_labeled_graph,
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
local builtin_namespace = require("luasnip.util._builtin_vars")
|
||||
|
||||
local function tbl_to_lazy_env(tbl)
|
||||
local function wrapper(varname)
|
||||
local val_ = tbl[varname]
|
||||
if type(val_) == "function" then
|
||||
return val_()
|
||||
end
|
||||
return val_
|
||||
end
|
||||
|
||||
return wrapper
|
||||
end
|
||||
|
||||
local namespaces = {}
|
||||
|
||||
-- Namespaces allow users to define their own environmet variables
|
||||
local function _resolve_namespace_var(full_varname)
|
||||
local parts = vim.split(full_varname, "_")
|
||||
local nmsp = namespaces[parts[1]]
|
||||
|
||||
local varname
|
||||
-- Is safe to fallback to the buitin-unnamed namespace as the checks in _env_namespace
|
||||
-- don't allow overriding those vars
|
||||
if nmsp then
|
||||
varname = full_varname:sub(#parts[1] + 2)
|
||||
else
|
||||
nmsp = namespaces[""]
|
||||
varname = full_varname
|
||||
end
|
||||
return nmsp, varname
|
||||
end
|
||||
|
||||
local Environ = {}
|
||||
|
||||
function Environ.is_table(var_fullname)
|
||||
local nmsp, varname = _resolve_namespace_var(var_fullname)
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
return nmsp.is_table(varname)
|
||||
end
|
||||
|
||||
function Environ:new(info, o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
vim.list_extend(info, info.pos) -- For compatibility with old user defined namespaces
|
||||
|
||||
for ns_name, ns in pairs(namespaces) do
|
||||
local eager_vars = {}
|
||||
if ns.init then
|
||||
eager_vars = ns.init(info)
|
||||
end
|
||||
for _, eager in ipairs(ns.eager) do
|
||||
if not eager_vars[eager] then
|
||||
eager_vars[eager] = ns.vars(eager)
|
||||
end
|
||||
end
|
||||
|
||||
local prefix = ""
|
||||
if ns_name ~= "" then
|
||||
prefix = ns_name .. "_"
|
||||
end
|
||||
|
||||
for name, val in pairs(eager_vars) do
|
||||
name = prefix .. name
|
||||
rawset(o, name, val)
|
||||
end
|
||||
end
|
||||
return o
|
||||
end
|
||||
|
||||
local builtin_ns_names = vim.inspect(vim.tbl_keys(builtin_namespace.builtin_ns))
|
||||
|
||||
local function _env_namespace(name, opts)
|
||||
assert(
|
||||
opts and type(opts) == "table",
|
||||
("Your opts for '%s' has to be a table"):format(name)
|
||||
)
|
||||
assert(
|
||||
opts.init or opts.vars,
|
||||
("Your opts for '%s' needs init or vars"):format(name)
|
||||
)
|
||||
|
||||
-- namespace.eager → ns.vars
|
||||
assert(
|
||||
not opts.eager or opts.vars,
|
||||
("Your opts for %s can't set a `eager` field without the `vars` one"):format(
|
||||
name
|
||||
)
|
||||
)
|
||||
|
||||
opts.eager = opts.eager or {}
|
||||
local multiline_vars = opts.multiline_vars or false
|
||||
|
||||
local type_of_it = type(multiline_vars)
|
||||
|
||||
assert(
|
||||
type_of_it == "table"
|
||||
or type_of_it == "boolean"
|
||||
or type_of_it == "function",
|
||||
("Your opts for %s can't have `multiline_vars` of type %s"):format(
|
||||
name,
|
||||
type_of_it
|
||||
)
|
||||
)
|
||||
|
||||
-- If type is function we don't have to override it
|
||||
if type_of_it == "table" then
|
||||
local is_table_set = {}
|
||||
|
||||
for _, key in ipairs(multiline_vars) do
|
||||
is_table_set[key] = true
|
||||
end
|
||||
|
||||
opts.is_table = function(key)
|
||||
return is_table_set[key] or false
|
||||
end
|
||||
elseif type_of_it == "boolean" then
|
||||
opts.is_table = function(_)
|
||||
return multiline_vars
|
||||
end
|
||||
else -- is a function
|
||||
opts.is_table = multiline_vars
|
||||
end
|
||||
|
||||
if opts.vars and type(opts.vars) == "table" then
|
||||
opts.vars = tbl_to_lazy_env(opts.vars)
|
||||
end
|
||||
|
||||
namespaces[name] = opts
|
||||
end
|
||||
|
||||
_env_namespace("", builtin_namespace)
|
||||
|
||||
-- The exposed api checks for the names to avoid accidental overrides
|
||||
function Environ.env_namespace(name, opts)
|
||||
assert(
|
||||
name:match("^[a-zA-Z][a-zA-Z0-9]*$"),
|
||||
("You can't create a namespace with name '%s' it has to contain only and at least a non alpha-numeric character"):format(
|
||||
name
|
||||
)
|
||||
)
|
||||
assert(
|
||||
not builtin_namespace.builtin_ns[name],
|
||||
("You can't create a namespace with name '%s' because is one one of %s"):format(
|
||||
name,
|
||||
builtin_ns_names
|
||||
)
|
||||
)
|
||||
|
||||
_env_namespace(name, opts)
|
||||
end
|
||||
|
||||
function Environ:__index(key)
|
||||
local nmsp, varname = _resolve_namespace_var(key)
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local val = nmsp.vars(varname)
|
||||
|
||||
rawset(self, key, val)
|
||||
|
||||
return val
|
||||
end
|
||||
|
||||
function Environ:override(env, new_env)
|
||||
for k, v in pairs(new_env) do
|
||||
env[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
local fake_env = {
|
||||
__index = function(tbl, key)
|
||||
local var
|
||||
if Environ.is_table(key) then
|
||||
var = { "$" .. key }
|
||||
else
|
||||
var = "$" .. key
|
||||
end
|
||||
rawset(tbl, key, var)
|
||||
return var
|
||||
end,
|
||||
}
|
||||
|
||||
function Environ.fake()
|
||||
local o = {}
|
||||
setmetatable(o, fake_env)
|
||||
return o
|
||||
end
|
||||
|
||||
return Environ
|
||||
@ -0,0 +1,18 @@
|
||||
local node_names = require("luasnip.util.types").names_pascal_case
|
||||
|
||||
return {
|
||||
enter = 1,
|
||||
leave = 2,
|
||||
change_choice = 3,
|
||||
pre_expand = 4,
|
||||
to_string = function(node_type, event_id)
|
||||
if event_id == 3 then
|
||||
return "ChangeChoice"
|
||||
elseif event_id == 4 then
|
||||
return "PreExpand"
|
||||
else
|
||||
return node_names[node_type]
|
||||
.. (event_id == 1 and "Enter" or "Leave")
|
||||
end
|
||||
end,
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
-- eventually turn ext_opts into proper objects, mainly for
|
||||
-- default-construction eg. assured `complete`.
|
||||
--
|
||||
-- child_*-functions perform the same operation as theiry non-child
|
||||
-- counterparts, but on a collection (eg.
|
||||
-- `{[types.insertNode={...}, [types.textNode]= {...}]}`) of ext_opts.
|
||||
|
||||
local types = require("luasnip.util.types")
|
||||
|
||||
-- vim.tbl_extend always creates a new table, but doesn't accept nil, so we
|
||||
-- always pass this empty table, which will (has to!) stay empty.
|
||||
local shared_empty_table = {}
|
||||
|
||||
local states = {
|
||||
"active",
|
||||
"passive",
|
||||
"snippet_passive",
|
||||
"visited",
|
||||
"unvisited",
|
||||
}
|
||||
|
||||
-- opts: child_ext_opts, have to have hl_group set for all combinations of
|
||||
-- node-type and active,passive,snippet_passive,visited,unvisited.
|
||||
local function clear_invalid(opts)
|
||||
--stylua: ignore start
|
||||
for _, node_type in pairs(types.node_types) do
|
||||
for _, state in ipairs(states) do
|
||||
local state_hl_group = opts[node_type][state].hl_group
|
||||
|
||||
opts[node_type][state].hl_group =
|
||||
vim.fn.hlexists(state_hl_group) == 1 and state_hl_group
|
||||
or nil
|
||||
end
|
||||
end
|
||||
--stylua: ignore end
|
||||
end
|
||||
|
||||
local function _complete_ext_opts(ext_opts)
|
||||
if not ext_opts then
|
||||
ext_opts = {}
|
||||
end
|
||||
ext_opts.snippet_passive = ext_opts.snippet_passive or {}
|
||||
ext_opts.passive = vim.tbl_extend(
|
||||
"keep",
|
||||
ext_opts.passive or shared_empty_table,
|
||||
ext_opts.snippet_passive or shared_empty_table
|
||||
)
|
||||
-- both unvisited and visited inherit from passive.
|
||||
ext_opts.unvisited = vim.tbl_extend(
|
||||
"keep",
|
||||
ext_opts.unvisited or shared_empty_table,
|
||||
ext_opts.passive or shared_empty_table
|
||||
)
|
||||
ext_opts.visited = vim.tbl_extend(
|
||||
"keep",
|
||||
ext_opts.visited or shared_empty_table,
|
||||
ext_opts.passive or shared_empty_table
|
||||
)
|
||||
-- active inherits from visited.
|
||||
ext_opts.active = vim.tbl_extend(
|
||||
"keep",
|
||||
ext_opts.active or shared_empty_table,
|
||||
ext_opts.visited or shared_empty_table
|
||||
)
|
||||
|
||||
for _, state in ipairs(states) do
|
||||
--stylua: ignore start
|
||||
if ext_opts[state].hl_group and not
|
||||
ext_opts[state].priority then
|
||||
ext_opts[state].priority = 0
|
||||
end
|
||||
--stylua: ignore end
|
||||
end
|
||||
|
||||
return ext_opts
|
||||
end
|
||||
|
||||
-- active inherits unset values from passive, which in turn inherits from
|
||||
-- snippet_passive.
|
||||
-- Also make sure that all keys have a table, and are not nil!
|
||||
local function child_complete(ext_opts)
|
||||
for _, node_type in pairs(types.node_types) do
|
||||
ext_opts[node_type] = _complete_ext_opts(ext_opts[node_type])
|
||||
end
|
||||
ext_opts.base_prio = 0
|
||||
|
||||
return ext_opts
|
||||
end
|
||||
|
||||
local function complete(ext_opts)
|
||||
_complete_ext_opts(ext_opts)
|
||||
ext_opts.base_prio = 0
|
||||
|
||||
return ext_opts
|
||||
end
|
||||
|
||||
-- in-place adds opts of b to a, doesn't override.
|
||||
-- a/b: completed ext_opts, not nil.
|
||||
local function extend(opts_a, opts_b)
|
||||
for _, state in ipairs(states) do
|
||||
opts_a[state] = vim.tbl_extend("keep", opts_a[state], opts_b[state])
|
||||
end
|
||||
|
||||
return opts_a
|
||||
end
|
||||
|
||||
-- in-place adds opts of b to a, doesn't override.
|
||||
-- a/b: completed child_ext_opts, not nil.
|
||||
local function child_extend(opts_a, opts_b)
|
||||
for _, node_type in ipairs(types.node_types) do
|
||||
extend(opts_a[node_type], opts_b[node_type])
|
||||
end
|
||||
|
||||
return opts_a
|
||||
end
|
||||
|
||||
local function increase_prio(opts, inc)
|
||||
-- increase only if there is a priority.
|
||||
for _, state in ipairs(states) do
|
||||
opts[state].priority = opts[state].priority
|
||||
and (opts[state].priority + inc)
|
||||
end
|
||||
end
|
||||
|
||||
-- ext_opts-priorities are defined relative to some base-priority.
|
||||
-- As nvim_api_buf_set_extmark takes absolute values only, we have to
|
||||
-- set the absolute priorities, which can vary depending on nesting-level
|
||||
-- of a given snippet, during runtime, by increasing the relative priorities by
|
||||
-- either the conf.base_prio or the base-prio used in the previous nesting-level.
|
||||
local function set_abs_prio(opts, new_base_prio)
|
||||
-- undo previous increase.
|
||||
-- base_prio is initialized with 0.
|
||||
local prio_offset = new_base_prio - opts.base_prio
|
||||
opts.base_prio = new_base_prio
|
||||
increase_prio(opts, prio_offset)
|
||||
|
||||
return opts
|
||||
end
|
||||
|
||||
local function child_set_abs_prio(opts, new_base_prio)
|
||||
-- undo previous increase.
|
||||
-- base_prio is initialized with 0.
|
||||
local prio_offset = new_base_prio - opts.base_prio
|
||||
opts.base_prio = new_base_prio
|
||||
for _, node_type in ipairs(types.node_types) do
|
||||
increase_prio(opts[node_type], prio_offset)
|
||||
end
|
||||
return opts
|
||||
end
|
||||
|
||||
return {
|
||||
clear_invalid = clear_invalid,
|
||||
complete = complete,
|
||||
child_complete = child_complete,
|
||||
extend = extend,
|
||||
child_extend = child_extend,
|
||||
set_abs_prio = set_abs_prio,
|
||||
child_set_abs_prio = child_set_abs_prio,
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
local M = {}
|
||||
|
||||
-- map fn -> {arg_indx = int, extend = fn}[]
|
||||
local function_properties = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local function default_extend(arg, extend)
|
||||
return vim.tbl_extend("keep", arg or {}, extend or {})
|
||||
end
|
||||
|
||||
---Create a new decorated version of `fn`.
|
||||
---@param fn The function to create a decorator for.
|
||||
---@vararg The values to extend with. These should match the descriptions passed
|
||||
---in `register`:
|
||||
---```lua
|
||||
---local function somefn(arg1, arg2, opts1, opts2)
|
||||
---...
|
||||
---end
|
||||
---register(somefn, {arg_indx=4}, {arg_indx=3})
|
||||
---apply(somefn,
|
||||
--- {key = "opts2 is extended with this"},
|
||||
--- {key = "and opts1 with this"})
|
||||
---```
|
||||
---@return function: The decorated function.
|
||||
function M.apply(fn, ...)
|
||||
local extend_properties = function_properties[fn]
|
||||
assert(
|
||||
extend_properties,
|
||||
"Cannot extend this function, it was not registered! Check :h luasnip-extend_decorator for more infos."
|
||||
)
|
||||
|
||||
local extend_values = { ... }
|
||||
|
||||
local decorated_fn = function(...)
|
||||
local direct_args = { ... }
|
||||
|
||||
-- override values of direct argument.
|
||||
for i, ep in ipairs(extend_properties) do
|
||||
local arg_indx = ep.arg_indx
|
||||
|
||||
-- still allow overriding with directly-passed keys.
|
||||
direct_args[arg_indx] =
|
||||
ep.extend(direct_args[arg_indx], extend_values[i])
|
||||
end
|
||||
|
||||
-- important: http://www.lua.org/manual/5.3/manual.html#3.4
|
||||
-- Passing arguments after the results from `unpack` would mess all this
|
||||
-- up.
|
||||
return fn(unpack(direct_args))
|
||||
end
|
||||
|
||||
-- we know how to extend the decorated function!
|
||||
function_properties[decorated_fn] = extend_properties
|
||||
|
||||
return decorated_fn
|
||||
end
|
||||
|
||||
---Prepare a function for usage with extend_decorator.
|
||||
---To create a decorated function which extends `opts`-style tables passed to it, we need to know
|
||||
--- 1. which parameter-position the opts are in and
|
||||
--- 2. how to extend them.
|
||||
---@param fn function: the function that should be registered.
|
||||
---@vararg tables. Each describes how to extend one parameter to `fn`.
|
||||
---The tables accept the following keys:
|
||||
--- - arg_indx, number (required): the position of the parameter to override.
|
||||
--- - extend, fn(arg, extend_value) -> effective_arg (optional): this function
|
||||
--- is used to extend the args passed to the decorated function.
|
||||
--- It defaults to a function which just extends the the arg-table with the
|
||||
--- extend-table.
|
||||
--- This extend-behaviour is adaptable to accomodate `s`, where the first
|
||||
--- argument may be string or table.
|
||||
function M.register(fn, ...)
|
||||
local fn_eps = { ... }
|
||||
|
||||
-- make sure ep.extend is set.
|
||||
for _, ep in ipairs(fn_eps) do
|
||||
ep.extend = ep.extend or default_extend
|
||||
end
|
||||
|
||||
function_properties[fn] = fn_eps
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,49 @@
|
||||
local sn = require("luasnip.nodes.snippet").SN
|
||||
local t = require("luasnip.nodes.textNode").T
|
||||
|
||||
return {
|
||||
var = function(_, _, node, text)
|
||||
local v = node.parent.snippet.env[text]
|
||||
if type(v) == "table" then
|
||||
-- Avoid issues with empty vars
|
||||
if #v > 0 then
|
||||
return v
|
||||
else
|
||||
return { "" }
|
||||
end
|
||||
else
|
||||
return { v }
|
||||
end
|
||||
end,
|
||||
better_var = function(varname)
|
||||
return function(_, parent)
|
||||
local v = parent.snippet.env[varname]
|
||||
if type(v) == "table" then
|
||||
-- Avoid issues with empty vars
|
||||
if #v > 0 then
|
||||
return v
|
||||
else
|
||||
return { "" }
|
||||
end
|
||||
else
|
||||
return { v }
|
||||
end
|
||||
end
|
||||
end,
|
||||
eval_vim_dynamic = function(vimstring)
|
||||
return function()
|
||||
-- 'echo'd string is returned to lua.
|
||||
return sn(nil, {
|
||||
t(
|
||||
vim.split(
|
||||
vim.api.nvim_exec("echo " .. vimstring, true),
|
||||
"\n"
|
||||
)
|
||||
),
|
||||
})
|
||||
end
|
||||
end,
|
||||
copy = function(args)
|
||||
return args[1]
|
||||
end,
|
||||
}
|
||||
@ -0,0 +1,469 @@
|
||||
-- Taken from https://github.com/actboy168/json.lua
|
||||
|
||||
-- MIT License
|
||||
--
|
||||
-- Copyright (c) 2020 actboy168
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
-- of this software and associated documentation files (the "Software"), to deal
|
||||
-- in the Software without restriction, including without limitation the rights
|
||||
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
-- copies of the Software, and to permit persons to whom the Software is
|
||||
-- furnished to do so, subject to the following conditions:
|
||||
--
|
||||
-- The above copyright notice and this permission notice shall be included in all
|
||||
-- copies or substantial portions of the Software.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
-- SOFTWARE.
|
||||
|
||||
local type = type
|
||||
local next = next
|
||||
local error = error
|
||||
local tonumber = tonumber
|
||||
local string_char = string.char
|
||||
local string_byte = string.byte
|
||||
local string_find = string.find
|
||||
local string_match = string.match
|
||||
local string_gsub = string.gsub
|
||||
local string_sub = string.sub
|
||||
local string_format = string.format
|
||||
|
||||
local utf8_char
|
||||
|
||||
if _VERSION == "Lua 5.1" or _VERSION == "Lua 5.2" then
|
||||
local math_floor = math.floor
|
||||
function utf8_char(c)
|
||||
if c <= 0x7f then
|
||||
return string_char(c)
|
||||
elseif c <= 0x7ff then
|
||||
return string_char(math_floor(c / 64) + 192, c % 64 + 128)
|
||||
elseif c <= 0xffff then
|
||||
return string_char(
|
||||
math_floor(c / 4096) + 224,
|
||||
math_floor(c % 4096 / 64) + 128,
|
||||
c % 64 + 128
|
||||
)
|
||||
elseif c <= 0x10ffff then
|
||||
return string_char(
|
||||
math_floor(c / 262144) + 240,
|
||||
math_floor(c % 262144 / 4096) + 128,
|
||||
math_floor(c % 4096 / 64) + 128,
|
||||
c % 64 + 128
|
||||
)
|
||||
end
|
||||
error(string_format("invalid UTF-8 code '%x'", c))
|
||||
end
|
||||
else
|
||||
utf8_char = utf8.char
|
||||
end
|
||||
|
||||
local encode_escape_map = {
|
||||
['"'] = '\\"',
|
||||
["\\"] = "\\\\",
|
||||
["/"] = "\\/",
|
||||
["\b"] = "\\b",
|
||||
["\f"] = "\\f",
|
||||
["\n"] = "\\n",
|
||||
["\r"] = "\\r",
|
||||
["\t"] = "\\t",
|
||||
}
|
||||
|
||||
local decode_escape_set = {}
|
||||
local decode_escape_map = {}
|
||||
for k, v in next, encode_escape_map do
|
||||
decode_escape_map[v] = k
|
||||
decode_escape_set[string_byte(v, 2)] = true
|
||||
end
|
||||
|
||||
local statusBuf
|
||||
local statusPos
|
||||
local statusTop
|
||||
local statusAry = {}
|
||||
local statusRef = {}
|
||||
|
||||
local function find_line()
|
||||
local line = 1
|
||||
local pos = 1
|
||||
while true do
|
||||
local f, _, nl1, nl2 = string_find(statusBuf, "([\n\r])([\n\r]?)", pos)
|
||||
if not f then
|
||||
return line, statusPos - pos + 1
|
||||
end
|
||||
local newpos = f + ((nl1 == nl2 or nl2 == "") and 1 or 2)
|
||||
if newpos > statusPos then
|
||||
return line, statusPos - pos + 1
|
||||
end
|
||||
pos = newpos
|
||||
line = line + 1
|
||||
end
|
||||
end
|
||||
|
||||
local function decode_error(msg)
|
||||
error(string_format("ERROR: %s at line %d col %d", msg, find_line()), 2)
|
||||
end
|
||||
|
||||
local function get_word()
|
||||
return string_match(statusBuf, "^[^ \t\r\n%]},]*", statusPos)
|
||||
end
|
||||
|
||||
local function skip_comment(b)
|
||||
if
|
||||
b ~= 47 --[[ '/' ]]
|
||||
then
|
||||
return
|
||||
end
|
||||
local c = string_byte(statusBuf, statusPos + 1)
|
||||
if
|
||||
c == 42 --[[ '*' ]]
|
||||
then
|
||||
-- block comment
|
||||
local pos = string_find(statusBuf, "*/", statusPos)
|
||||
if pos then
|
||||
statusPos = pos + 2
|
||||
else
|
||||
statusPos = #statusBuf + 1
|
||||
end
|
||||
return true
|
||||
elseif
|
||||
c == 47 --[[ '/' ]]
|
||||
then
|
||||
-- line comment
|
||||
local pos = string_find(statusBuf, "[\r\n]", statusPos)
|
||||
if pos then
|
||||
statusPos = pos
|
||||
else
|
||||
statusPos = #statusBuf + 1
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function next_byte()
|
||||
local pos = string_find(statusBuf, "[^ \t\r\n]", statusPos)
|
||||
if pos then
|
||||
statusPos = pos
|
||||
local b = string_byte(statusBuf, pos)
|
||||
if not skip_comment(b) then
|
||||
return b
|
||||
end
|
||||
return next_byte()
|
||||
end
|
||||
return -1
|
||||
end
|
||||
|
||||
local function decode_unicode_surrogate(s1, s2)
|
||||
return utf8_char(
|
||||
0x10000
|
||||
+ (tonumber(s1, 16) - 0xd800) * 0x400
|
||||
+ (tonumber(s2, 16) - 0xdc00)
|
||||
)
|
||||
end
|
||||
|
||||
local function decode_unicode_escape(s)
|
||||
return utf8_char(tonumber(s, 16))
|
||||
end
|
||||
|
||||
local function decode_string()
|
||||
local has_unicode_escape = false
|
||||
local has_escape = false
|
||||
local i = statusPos + 1
|
||||
while true do
|
||||
i = string_find(statusBuf, '[%z\1-\31\\"]', i)
|
||||
if not i then
|
||||
decode_error("expected closing quote for string")
|
||||
end
|
||||
local x = string_byte(statusBuf, i)
|
||||
if x < 32 then
|
||||
statusPos = i
|
||||
decode_error("control character in string")
|
||||
end
|
||||
if
|
||||
x == 34 --[[ '"' ]]
|
||||
then
|
||||
local s = string_sub(statusBuf, statusPos + 1, i - 1)
|
||||
if has_unicode_escape then
|
||||
s = string_gsub(
|
||||
string_gsub(
|
||||
s,
|
||||
"\\u([dD][89aAbB]%x%x)\\u([dD][c-fC-F]%x%x)",
|
||||
decode_unicode_surrogate
|
||||
),
|
||||
"\\u(%x%x%x%x)",
|
||||
decode_unicode_escape
|
||||
)
|
||||
end
|
||||
if has_escape then
|
||||
s = string_gsub(s, "\\.", decode_escape_map)
|
||||
end
|
||||
statusPos = i + 1
|
||||
return s
|
||||
end
|
||||
--assert(x == 92 --[[ "\\" ]])
|
||||
local nx = string_byte(statusBuf, i + 1)
|
||||
if
|
||||
nx == 117 --[[ "u" ]]
|
||||
then
|
||||
if not string_match(statusBuf, "^%x%x%x%x", i + 2) then
|
||||
statusPos = i
|
||||
decode_error("invalid unicode escape in string")
|
||||
end
|
||||
has_unicode_escape = true
|
||||
i = i + 6
|
||||
else
|
||||
if not decode_escape_set[nx] then
|
||||
statusPos = i
|
||||
decode_error(
|
||||
"invalid escape char '"
|
||||
.. (nx and string_char(nx) or "<eol>")
|
||||
.. "' in string"
|
||||
)
|
||||
end
|
||||
has_escape = true
|
||||
i = i + 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function decode_number()
|
||||
local num, c =
|
||||
string_match(statusBuf, "^([0-9]+%.?[0-9]*)([eE]?)", statusPos)
|
||||
if
|
||||
not num or string_byte(num, -1) == 0x2E --[[ "." ]]
|
||||
then
|
||||
decode_error("invalid number '" .. get_word() .. "'")
|
||||
end
|
||||
if c ~= "" then
|
||||
num = string_match(
|
||||
statusBuf,
|
||||
"^([^eE]*[eE][-+]?[0-9]+)[ \t\r\n%]},/]",
|
||||
statusPos
|
||||
)
|
||||
if not num then
|
||||
decode_error("invalid number '" .. get_word() .. "'")
|
||||
end
|
||||
end
|
||||
statusPos = statusPos + #num
|
||||
return tonumber(num)
|
||||
end
|
||||
|
||||
local function decode_number_zero()
|
||||
local num, c = string_match(statusBuf, "^(.%.?[0-9]*)([eE]?)", statusPos)
|
||||
if
|
||||
not num
|
||||
or string_byte(num, -1) == 0x2E --[[ "." ]]
|
||||
or string_match(statusBuf, "^.[0-9]+", statusPos)
|
||||
then
|
||||
decode_error("invalid number '" .. get_word() .. "'")
|
||||
end
|
||||
if c ~= "" then
|
||||
num = string_match(
|
||||
statusBuf,
|
||||
"^([^eE]*[eE][-+]?[0-9]+)[ \t\r\n%]},/]",
|
||||
statusPos
|
||||
)
|
||||
if not num then
|
||||
decode_error("invalid number '" .. get_word() .. "'")
|
||||
end
|
||||
end
|
||||
statusPos = statusPos + #num
|
||||
return tonumber(num)
|
||||
end
|
||||
|
||||
local function decode_number_negative()
|
||||
statusPos = statusPos + 1
|
||||
local c = string_byte(statusBuf, statusPos)
|
||||
if c then
|
||||
if c == 0x30 then
|
||||
return -decode_number_zero()
|
||||
elseif c > 0x30 and c < 0x3A then
|
||||
return -decode_number()
|
||||
end
|
||||
end
|
||||
decode_error("invalid number '" .. get_word() .. "'")
|
||||
end
|
||||
|
||||
local function decode_true()
|
||||
if string_sub(statusBuf, statusPos, statusPos + 3) ~= "true" then
|
||||
decode_error("invalid literal '" .. get_word() .. "'")
|
||||
end
|
||||
statusPos = statusPos + 4
|
||||
return true
|
||||
end
|
||||
|
||||
local function decode_false()
|
||||
if string_sub(statusBuf, statusPos, statusPos + 4) ~= "false" then
|
||||
decode_error("invalid literal '" .. get_word() .. "'")
|
||||
end
|
||||
statusPos = statusPos + 5
|
||||
return false
|
||||
end
|
||||
|
||||
local function decode_null()
|
||||
if string_sub(statusBuf, statusPos, statusPos + 3) ~= "null" then
|
||||
decode_error("invalid literal '" .. get_word() .. "'")
|
||||
end
|
||||
statusPos = statusPos + 4
|
||||
return vim.NIL
|
||||
end
|
||||
|
||||
local function decode_array()
|
||||
statusPos = statusPos + 1
|
||||
local res = {}
|
||||
local chr = next_byte()
|
||||
if
|
||||
chr == 93 --[[ ']' ]]
|
||||
then
|
||||
statusPos = statusPos + 1
|
||||
return res
|
||||
end
|
||||
statusTop = statusTop + 1
|
||||
statusAry[statusTop] = true
|
||||
statusRef[statusTop] = res
|
||||
return res
|
||||
end
|
||||
|
||||
local function decode_object()
|
||||
statusPos = statusPos + 1
|
||||
local res = {}
|
||||
local chr = next_byte()
|
||||
if
|
||||
chr == 125 --[[ ']' ]]
|
||||
then
|
||||
statusPos = statusPos + 1
|
||||
return vim.empty_dict()
|
||||
end
|
||||
statusTop = statusTop + 1
|
||||
statusAry[statusTop] = false
|
||||
statusRef[statusTop] = res
|
||||
return res
|
||||
end
|
||||
|
||||
local decode_uncompleted_map = {
|
||||
[string_byte('"')] = decode_string,
|
||||
[string_byte("0")] = decode_number_zero,
|
||||
[string_byte("1")] = decode_number,
|
||||
[string_byte("2")] = decode_number,
|
||||
[string_byte("3")] = decode_number,
|
||||
[string_byte("4")] = decode_number,
|
||||
[string_byte("5")] = decode_number,
|
||||
[string_byte("6")] = decode_number,
|
||||
[string_byte("7")] = decode_number,
|
||||
[string_byte("8")] = decode_number,
|
||||
[string_byte("9")] = decode_number,
|
||||
[string_byte("-")] = decode_number_negative,
|
||||
[string_byte("t")] = decode_true,
|
||||
[string_byte("f")] = decode_false,
|
||||
[string_byte("n")] = decode_null,
|
||||
[string_byte("[")] = decode_array,
|
||||
[string_byte("{")] = decode_object,
|
||||
}
|
||||
local function unexpected_character()
|
||||
decode_error(
|
||||
"unexpected character '"
|
||||
.. string_sub(statusBuf, statusPos, statusPos)
|
||||
.. "'"
|
||||
)
|
||||
end
|
||||
local function unexpected_eol()
|
||||
decode_error("unexpected character '<eol>'")
|
||||
end
|
||||
|
||||
local decode_map = {}
|
||||
for i = 0, 255 do
|
||||
decode_map[i] = decode_uncompleted_map[i] or unexpected_character
|
||||
end
|
||||
decode_map[-1] = unexpected_eol
|
||||
|
||||
local function decode()
|
||||
return decode_map[next_byte()]()
|
||||
end
|
||||
|
||||
local function decode_item()
|
||||
local top = statusTop
|
||||
local ref = statusRef[top]
|
||||
if statusAry[top] then
|
||||
ref[#ref + 1] = decode()
|
||||
else
|
||||
local key = decode_string()
|
||||
if
|
||||
next_byte() ~= 58 --[[ ':' ]]
|
||||
then
|
||||
decode_error("expected ':'")
|
||||
end
|
||||
statusPos = statusPos + 1
|
||||
ref[key] = decode()
|
||||
end
|
||||
if top == statusTop then
|
||||
repeat
|
||||
local chr = next_byte()
|
||||
statusPos = statusPos + 1
|
||||
if
|
||||
chr == 44 --[[ "," ]]
|
||||
then
|
||||
local c = next_byte()
|
||||
if statusAry[statusTop] then
|
||||
if
|
||||
c ~= 93 --[[ "]" ]]
|
||||
then
|
||||
return
|
||||
end
|
||||
else
|
||||
if
|
||||
c ~= 125 --[[ "}" ]]
|
||||
then
|
||||
return
|
||||
end
|
||||
end
|
||||
statusPos = statusPos + 1
|
||||
else
|
||||
if statusAry[statusTop] then
|
||||
if
|
||||
chr ~= 93 --[[ "]" ]]
|
||||
then
|
||||
decode_error("expected ']' or ','")
|
||||
end
|
||||
else
|
||||
if
|
||||
chr ~= 125 --[[ "}" ]]
|
||||
then
|
||||
decode_error("expected '}' or ','")
|
||||
end
|
||||
end
|
||||
end
|
||||
statusTop = statusTop - 1
|
||||
until statusTop == 0
|
||||
end
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
if str == "" then
|
||||
error("attempted to decode an empty string")
|
||||
end
|
||||
statusBuf = str
|
||||
statusPos = 1
|
||||
statusTop = 0
|
||||
if next_byte() == -1 then
|
||||
return vim.NIL
|
||||
end
|
||||
local res = decode()
|
||||
while statusTop > 0 do
|
||||
decode_item()
|
||||
end
|
||||
if string_find(statusBuf, "[^ \t\r\n]", statusPos) then
|
||||
decode_error("trailing garbage")
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,41 @@
|
||||
local Path = require("luasnip.util.path")
|
||||
|
||||
-- neovim-loader does not handle module-names with dots correctly, so for
|
||||
-- jsregexp-0.0.6, the call to require("jsregexp.core") in jsregexp.lua errors
|
||||
-- even if the library is in rtp.
|
||||
-- Resolve path to jsregexp.so manually, and loadlib it in preload (and remove
|
||||
-- preload after requires are done and have failed/worked).
|
||||
|
||||
-- omit "@".
|
||||
local this_file = debug.getinfo(1).source:sub(2)
|
||||
local repo_dir = vim.fn.fnamemodify(this_file, ":h:h:h:h")
|
||||
local jsregexp_core_path = Path.join(repo_dir, "deps", "luasnip-jsregexp.so")
|
||||
|
||||
-- rather gracefully, if the path does not exist, or loadlib can't do its job
|
||||
-- for some other reason, the preload will be set to nil, ie not be set.
|
||||
--
|
||||
-- This means we don't hinder a regularly installed 0.0.6-jsregexp-library,
|
||||
-- since its `require("jsregexp.core")` will be unaffected.
|
||||
package.preload["jsregexp.core"] =
|
||||
package.loadlib(jsregexp_core_path, "luaopen_jsregexp_core")
|
||||
|
||||
-- jsregexp: first try loading the version installed by luasnip, then global ones.
|
||||
local jsregexp_ok, jsregexp = pcall(require, "luasnip-jsregexp")
|
||||
if not jsregexp_ok then
|
||||
jsregexp_ok, jsregexp = pcall(require, "jsregexp")
|
||||
end
|
||||
|
||||
-- don't want to affect other requires.
|
||||
package.preload["jsregexp.core"] = nil
|
||||
|
||||
if not jsregexp_ok then
|
||||
return false
|
||||
end
|
||||
|
||||
-- detect version, and return compile-function.
|
||||
-- 0.0.6-compile_safe and 0.0.5-compile behave the same, ie. nil, err on error.
|
||||
if jsregexp.compile_safe then
|
||||
return jsregexp.compile_safe
|
||||
else
|
||||
return jsregexp.compile
|
||||
end
|
||||
@ -0,0 +1,13 @@
|
||||
return function(lazy_t, lazy_defs)
|
||||
return setmetatable(lazy_t, {
|
||||
__index = function(t, k)
|
||||
local v = lazy_defs[k]
|
||||
if v then
|
||||
local v_resolved = v()
|
||||
rawset(t, k, v_resolved)
|
||||
return v_resolved
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
})
|
||||
end
|
||||
@ -0,0 +1,123 @@
|
||||
local util = require("luasnip.util.util")
|
||||
|
||||
-- older neovim-versions (even 0.7.2) do not have stdpath("log").
|
||||
local logpath_ok, logpath = pcall(vim.fn.stdpath, "log")
|
||||
if not logpath_ok then
|
||||
logpath = vim.fn.stdpath("cache")
|
||||
end
|
||||
|
||||
-- just to be sure this dir exists.
|
||||
-- 448 = 0700
|
||||
vim.loop.fs_mkdir(logpath, 448)
|
||||
|
||||
local log_location = logpath .. "/luasnip.log"
|
||||
local log_old_location = logpath .. "/luasnip.log.old"
|
||||
|
||||
local luasnip_log_fd = vim.loop.fs_open(
|
||||
log_location,
|
||||
-- only append.
|
||||
"a",
|
||||
-- 420 = 0644
|
||||
420
|
||||
)
|
||||
|
||||
local function log_line_append(msg)
|
||||
msg = msg:gsub("\n", "\n | ")
|
||||
vim.loop.fs_write(luasnip_log_fd, msg .. "\n")
|
||||
end
|
||||
|
||||
if not luasnip_log_fd then
|
||||
-- print a warning
|
||||
print(
|
||||
("LuaSnip: could not open log at %s. Not logging for this session."):format(
|
||||
log_location
|
||||
)
|
||||
)
|
||||
-- make log_line_append do nothing.
|
||||
log_line_append = util.nop
|
||||
else
|
||||
-- if log_fd found, check if log should be rotated.
|
||||
local logsize = vim.loop.fs_fstat(luasnip_log_fd).size
|
||||
if logsize > 10 * 2 ^ 20 then
|
||||
-- logsize > 10MiB:
|
||||
-- move log -> old log, start new log.
|
||||
vim.loop.fs_rename(log_location, log_old_location)
|
||||
luasnip_log_fd = vim.loop.fs_open(
|
||||
log_location,
|
||||
-- only append.
|
||||
"a",
|
||||
-- 420 = 0644
|
||||
420
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
local log = {
|
||||
error = function(msg)
|
||||
log_line_append("ERROR | " .. msg)
|
||||
end,
|
||||
warn = function(msg)
|
||||
log_line_append("WARN | " .. msg)
|
||||
end,
|
||||
info = function(msg)
|
||||
log_line_append("INFO | " .. msg)
|
||||
end,
|
||||
debug = function(msg)
|
||||
log_line_append("DEBUG | " .. msg)
|
||||
end,
|
||||
}
|
||||
|
||||
-- functions copied directly by deepcopy.
|
||||
-- will be initialized later on, by set_loglevel.
|
||||
local effective_log
|
||||
|
||||
-- levels sorted by importance, descending.
|
||||
local loglevels = { "error", "warn", "info", "debug" }
|
||||
|
||||
-- special key none disable all logging.
|
||||
function M.set_loglevel(target_level)
|
||||
local target_level_indx = util.indx_of(loglevels, target_level)
|
||||
if target_level == "none" then
|
||||
target_level_indx = 0
|
||||
end
|
||||
|
||||
assert(target_level_indx ~= nil, "invalid level!")
|
||||
|
||||
-- reset effective loglevels, set those with importance higher than
|
||||
-- target_level, disable (nop) those with lower.
|
||||
effective_log = {}
|
||||
for i = 1, target_level_indx do
|
||||
effective_log[loglevels[i]] = log[loglevels[i]]
|
||||
end
|
||||
for i = target_level_indx + 1, #loglevels do
|
||||
effective_log[loglevels[i]] = util.nop
|
||||
end
|
||||
end
|
||||
|
||||
function M.new(module_name)
|
||||
local module_log = {}
|
||||
for name, _ in pairs(log) do
|
||||
module_log[name] = function(msg, ...)
|
||||
-- don't immediately get the referenced function, we'd like to
|
||||
-- allow changing the loglevel on-the-fly.
|
||||
effective_log[name](module_name .. ": " .. msg:format(...))
|
||||
end
|
||||
end
|
||||
return module_log
|
||||
end
|
||||
|
||||
function M.open()
|
||||
vim.cmd(("tabnew %s"):format(log_location))
|
||||
end
|
||||
|
||||
-- to verify log is working.
|
||||
function M.ping()
|
||||
log_line_append(("PONG | pong! (%s)"):format(os.date()))
|
||||
end
|
||||
|
||||
-- set default-loglevel.
|
||||
M.set_loglevel("warn")
|
||||
|
||||
return M
|
||||
@ -0,0 +1,208 @@
|
||||
local session = require("luasnip.session")
|
||||
|
||||
local Mark = {}
|
||||
|
||||
function Mark:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return o
|
||||
end
|
||||
|
||||
-- opts just like in nvim_buf_set_extmark.
|
||||
local function mark(pos_begin, pos_end, opts)
|
||||
return Mark:new({
|
||||
id = vim.api.nvim_buf_set_extmark(
|
||||
0,
|
||||
session.ns_id,
|
||||
pos_begin[1],
|
||||
pos_begin[2],
|
||||
-- override end_* in opts.
|
||||
vim.tbl_extend(
|
||||
"force",
|
||||
opts,
|
||||
{ end_line = pos_end[1], end_col = pos_end[2] }
|
||||
)
|
||||
),
|
||||
-- store opts here, can't be queried using nvim_buf_get_extmark_by_id.
|
||||
opts = opts,
|
||||
})
|
||||
end
|
||||
|
||||
local function bytecol_to_utfcol(pos)
|
||||
local line = vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)
|
||||
-- line[1]: get_lines returns table.
|
||||
-- use utf16-index.
|
||||
local utf16_indx, _ = vim.str_utfindex(line[1] or "", pos[2])
|
||||
return { pos[1], utf16_indx }
|
||||
end
|
||||
|
||||
function Mark:pos_begin_end()
|
||||
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
|
||||
0,
|
||||
session.ns_id,
|
||||
self.id,
|
||||
{ details = true }
|
||||
)
|
||||
|
||||
return bytecol_to_utfcol({ mark_info[1], mark_info[2] }),
|
||||
bytecol_to_utfcol({ mark_info[3].end_row, mark_info[3].end_col })
|
||||
end
|
||||
|
||||
function Mark:pos_begin()
|
||||
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
|
||||
0,
|
||||
session.ns_id,
|
||||
self.id,
|
||||
{ details = false }
|
||||
)
|
||||
|
||||
return bytecol_to_utfcol({ mark_info[1], mark_info[2] })
|
||||
end
|
||||
|
||||
function Mark:pos_end()
|
||||
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
|
||||
0,
|
||||
session.ns_id,
|
||||
self.id,
|
||||
{ details = true }
|
||||
)
|
||||
|
||||
return bytecol_to_utfcol({ mark_info[3].end_row, mark_info[3].end_col })
|
||||
end
|
||||
|
||||
function Mark:pos_begin_end_raw()
|
||||
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
|
||||
0,
|
||||
session.ns_id,
|
||||
self.id,
|
||||
{ details = true }
|
||||
)
|
||||
return { mark_info[1], mark_info[2] }, {
|
||||
mark_info[3].end_row,
|
||||
mark_info[3].end_col,
|
||||
}
|
||||
end
|
||||
|
||||
function Mark:pos_begin_raw()
|
||||
local mark_info = vim.api.nvim_buf_get_extmark_by_id(
|
||||
0,
|
||||
session.ns_id,
|
||||
self.id,
|
||||
{ details = false }
|
||||
)
|
||||
return { mark_info[1], mark_info[2] }
|
||||
end
|
||||
|
||||
function Mark:copy_pos_gravs(opts)
|
||||
local pos_beg, pos_end = self:pos_begin_end_raw()
|
||||
opts.right_gravity = self.opts.right_gravity
|
||||
opts.end_right_gravity = self.opts.end_right_gravity
|
||||
return mark(pos_beg, pos_end, opts)
|
||||
end
|
||||
|
||||
-- opts just like in nvim_buf_set_extmark.
|
||||
-- opts as first arg bcs. pos are pretty likely to stay the same.
|
||||
function Mark:update(opts, pos_begin, pos_end)
|
||||
-- if one is changed, the other is likely as well.
|
||||
if not pos_begin then
|
||||
pos_begin = old_pos_begin
|
||||
if not pos_end then
|
||||
pos_end = old_pos_end
|
||||
end
|
||||
end
|
||||
-- override with new.
|
||||
self.opts = vim.tbl_extend("force", self.opts, opts)
|
||||
vim.api.nvim_buf_set_extmark(
|
||||
0,
|
||||
session.ns_id,
|
||||
pos_begin[1],
|
||||
pos_begin[2],
|
||||
vim.tbl_extend(
|
||||
"force",
|
||||
self.opts,
|
||||
{ id = self.id, end_line = pos_end[1], end_col = pos_end[2] }
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
function Mark:set_opts(opts)
|
||||
local pos_begin, pos_end = self:pos_begin_end_raw()
|
||||
vim.api.nvim_buf_del_extmark(0, session.ns_id, self.id)
|
||||
|
||||
self.opts = opts
|
||||
-- set new extmark, current behaviour for updating seems inconsistent,
|
||||
-- eg. gravs are reset, deco is kept.
|
||||
self.id = vim.api.nvim_buf_set_extmark(
|
||||
0,
|
||||
session.ns_id,
|
||||
pos_begin[1],
|
||||
pos_begin[2],
|
||||
vim.tbl_extend(
|
||||
"force",
|
||||
opts,
|
||||
{ end_line = pos_end[1], end_col = pos_end[2] }
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
function Mark:set_rgravs(rgrav_left, rgrav_right)
|
||||
-- don't update if nothing would change.
|
||||
if
|
||||
self.opts.right_gravity ~= rgrav_left
|
||||
or self.opts.end_right_gravity ~= rgrav_right
|
||||
then
|
||||
self.opts.right_gravity = rgrav_left
|
||||
self.opts.end_right_gravity = rgrav_right
|
||||
self:set_opts(self.opts)
|
||||
end
|
||||
end
|
||||
|
||||
function Mark:get_rgrav(which)
|
||||
if which == -1 then
|
||||
return self.opts.right_gravity
|
||||
else
|
||||
return self.opts.end_right_gravity
|
||||
end
|
||||
end
|
||||
|
||||
function Mark:set_rgrav(which, rgrav)
|
||||
if which == -1 then
|
||||
if self.opts.right_gravity == rgrav then
|
||||
return
|
||||
end
|
||||
self.opts.right_gravity = rgrav
|
||||
else
|
||||
if self.opts.end_right_gravity == rgrav then
|
||||
return
|
||||
end
|
||||
self.opts.end_right_gravity = rgrav
|
||||
end
|
||||
self:set_opts(self.opts)
|
||||
end
|
||||
|
||||
function Mark:get_endpoint(which)
|
||||
-- simpler for now, look into perf here later.
|
||||
local l, r = self:pos_begin_end_raw()
|
||||
if which == -1 then
|
||||
return l
|
||||
else
|
||||
return r
|
||||
end
|
||||
end
|
||||
|
||||
-- change all opts except rgravs.
|
||||
function Mark:update_opts(opts)
|
||||
local opts_cp = vim.deepcopy(opts)
|
||||
opts_cp.right_gravity = self.opts.right_gravity
|
||||
opts_cp.end_right_gravity = self.opts.end_right_gravity
|
||||
self:set_opts(opts_cp)
|
||||
end
|
||||
|
||||
function Mark:clear()
|
||||
vim.api.nvim_buf_del_extmark(0, session.ns_id, self.id)
|
||||
end
|
||||
|
||||
return {
|
||||
mark = mark,
|
||||
}
|
||||
@ -0,0 +1,350 @@
|
||||
local ast_utils = require("luasnip.util.parser.ast_utils")
|
||||
local Ast = require("luasnip.util.parser.neovim_ast")
|
||||
local tNode = require("luasnip.nodes.textNode")
|
||||
local iNode = require("luasnip.nodes.insertNode")
|
||||
local fNode = require("luasnip.nodes.functionNode")
|
||||
local cNode = require("luasnip.nodes.choiceNode")
|
||||
local dNode = require("luasnip.nodes.dynamicNode")
|
||||
local sNode = require("luasnip.nodes.snippet")
|
||||
local functions = require("luasnip.util.functions")
|
||||
local Environ = require("luasnip.util.environ")
|
||||
local session = require("luasnip.session")
|
||||
local util = require("luasnip.util.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
local _split = function(s)
|
||||
return vim.split(s, "\n", { plain = true })
|
||||
end
|
||||
|
||||
local types = ast_utils.types
|
||||
|
||||
local to_node
|
||||
|
||||
local function fix_node_indices(nodes)
|
||||
local used_nodes = {}
|
||||
for _, node in ipairs(nodes) do
|
||||
if node.pos and node.pos > 0 then
|
||||
used_nodes[node.pos] = node
|
||||
end
|
||||
end
|
||||
|
||||
for _, v, i in util.key_sorted_pairs(used_nodes) do
|
||||
v.pos = i
|
||||
end
|
||||
return nodes
|
||||
end
|
||||
|
||||
local function ast2luasnip_nodes(ast_nodes)
|
||||
local nodes = {}
|
||||
for i, ast_node in ipairs(ast_nodes) do
|
||||
nodes[i] = ast_node.parsed
|
||||
end
|
||||
|
||||
return fix_node_indices(nodes)
|
||||
end
|
||||
|
||||
local function var_func(ast)
|
||||
local varname = ast.name
|
||||
|
||||
local transform_func
|
||||
if ast.transform then
|
||||
transform_func = ast_utils.apply_transform(ast.transform)
|
||||
else
|
||||
transform_func = util.id
|
||||
end
|
||||
|
||||
return function(_, parent, _, variable_default)
|
||||
local v = parent.snippet.env[varname]
|
||||
local lines
|
||||
if type(v) == "table" then
|
||||
-- Avoid issues with empty vars
|
||||
if #v > 0 then
|
||||
lines = v
|
||||
else
|
||||
lines = { "" }
|
||||
end
|
||||
else
|
||||
lines = { v }
|
||||
end
|
||||
|
||||
-- quicker than checking `lines` in some way.
|
||||
if not v then
|
||||
-- the variable is not defined:
|
||||
-- insert the variable's name as a placeholder.
|
||||
return sNode.SN(nil, { iNode.I(1, varname) })
|
||||
end
|
||||
if #lines == 0 or (#lines == 1 and #lines[1] == 0) then
|
||||
-- The variable is empty.
|
||||
|
||||
-- default passed as user_arg, rationale described in
|
||||
-- types.VARIABLE-to_node_func.
|
||||
if variable_default then
|
||||
return variable_default
|
||||
else
|
||||
-- lines might still just be {} (#lines == 0).
|
||||
lines = { "" }
|
||||
end
|
||||
end
|
||||
|
||||
-- v exists and has no default, return the (maybe modified) lines.
|
||||
return sNode.SN(nil, { tNode.T(transform_func(lines)) })
|
||||
end
|
||||
end
|
||||
|
||||
local function copy_func(tabstop)
|
||||
local transform_func
|
||||
if tabstop.transform then
|
||||
transform_func = ast_utils.apply_transform(tabstop.transform)
|
||||
else
|
||||
transform_func = util.id
|
||||
end
|
||||
return function(args)
|
||||
return transform_func(args[1])
|
||||
end
|
||||
end
|
||||
|
||||
local function placeholder_func(_, parent, _, placeholder_snip)
|
||||
local env = parent.snippet.env
|
||||
-- is_interactive needs env to determine interactiveness.
|
||||
-- env is passed through to all following is_interactive calls.
|
||||
if not placeholder_snip:is_interactive(env) then
|
||||
-- this placeholder only contains text or (transformed)
|
||||
-- variables, so an insertNode can be generated from its
|
||||
-- contents.
|
||||
-- create new snippet that only contains the parsed snippetNode, so we
|
||||
-- can `fake_expand` and `get_static_text()` it.
|
||||
local snippet = sNode.S("", placeholder_snip)
|
||||
|
||||
-- get active env from snippet.
|
||||
snippet:fake_expand({ env = env })
|
||||
local iText = snippet:get_static_text()
|
||||
|
||||
-- no need to un-escape iText, that was already done.
|
||||
return sNode.SN(nil, iNode.I(1, iText))
|
||||
end
|
||||
|
||||
return sNode.SN(
|
||||
nil,
|
||||
session.config.parser_nested_assembler(1, placeholder_snip)
|
||||
)
|
||||
end
|
||||
|
||||
---If this tabstop-node (CHOICE, TABSTOP or PLACEHOLDER) is a copy of another,
|
||||
---set that up and return, otherwise return false.
|
||||
---@param ast table: ast-node.
|
||||
---@return boolean: whether the node is now parsed.
|
||||
local function tabstop_node_copy_inst(ast)
|
||||
local existing_tabstop_ast_node = ast.copies
|
||||
if existing_tabstop_ast_node then
|
||||
-- this tabstop is a mirror of an already-parsed tabstop/placeholder.
|
||||
ast.parsed =
|
||||
fNode.F(copy_func(ast), { existing_tabstop_ast_node.parsed })
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
-- these actually create nodes from any AST.
|
||||
local to_node_funcs = {
|
||||
-- careful! this parses the snippet into a list of nodes, not a full snippet!
|
||||
-- The table can then be passed to the regular snippet-constructors.
|
||||
[types.SNIPPET] = function(ast, _)
|
||||
ast.parsed = ast2luasnip_nodes(ast.children)
|
||||
end,
|
||||
[types.TEXT] = function(ast, _)
|
||||
local text = _split(ast.esc)
|
||||
ast.parsed = tNode.T(text)
|
||||
end,
|
||||
[types.CHOICE] = function(ast)
|
||||
-- even choices may be copies.
|
||||
if tabstop_node_copy_inst(ast) then
|
||||
return
|
||||
end
|
||||
|
||||
local choices = {}
|
||||
for i, choice in ipairs(ast.items) do
|
||||
choices[i] = tNode.T(_split(choice))
|
||||
end
|
||||
|
||||
ast.parsed = cNode.C(ast.tabstop, choices)
|
||||
end,
|
||||
[types.TABSTOP] = function(ast)
|
||||
if tabstop_node_copy_inst(ast) then
|
||||
return
|
||||
end
|
||||
-- tabstops don't have placeholder-text.
|
||||
ast.parsed = iNode.I(ast.tabstop)
|
||||
end,
|
||||
[types.PLACEHOLDER] = function(ast, state)
|
||||
if tabstop_node_copy_inst(ast) then
|
||||
return
|
||||
end
|
||||
|
||||
local node
|
||||
if #ast.children == 1 and ast.children[1].type == types.TEXT then
|
||||
-- we cannot place a dynamicNode as $0.
|
||||
-- But all valid ${0:some nodes here} contain just text inside
|
||||
-- them, so this works :)
|
||||
node = iNode.I(ast.tabstop, _split(ast.children[1].esc))
|
||||
else
|
||||
local snip = sNode.SN(1, ast2luasnip_nodes(ast.children))
|
||||
node = dNode.D(ast.tabstop, placeholder_func, {}, {
|
||||
-- pass snip here, again to preserve references to other tables.
|
||||
user_args = { snip },
|
||||
})
|
||||
end
|
||||
|
||||
ast.parsed = node
|
||||
end,
|
||||
[types.VARIABLE] = function(ast, state)
|
||||
local var = ast.name
|
||||
|
||||
local default
|
||||
if ast.children then
|
||||
default = sNode.SN(nil, ast2luasnip_nodes(ast.children))
|
||||
end
|
||||
|
||||
local fn
|
||||
local is_interactive_fn
|
||||
if state.var_functions[var] then
|
||||
fn, is_interactive_fn = unpack(state.var_functions[var])
|
||||
else
|
||||
fn = var_func(ast)
|
||||
-- override the regular `is_interactive` to accurately determine
|
||||
-- whether the snippet produced by the dynamicNode is interactive
|
||||
-- or not. This is important when a variable is wrapped inside a
|
||||
-- placeholder: ${1:$TM_SELECTED_TEXT}
|
||||
-- With variable-environments we cannot tell at parse-time whether
|
||||
-- the dynamicNode will be just text, an insertNode or some other
|
||||
-- nodes(the default), so that has to happen at runtime now.
|
||||
is_interactive_fn = function(_, env)
|
||||
local var_value = env[var]
|
||||
|
||||
if not var_value then
|
||||
-- inserts insertNode.
|
||||
return true
|
||||
end
|
||||
|
||||
-- just wrap it for more uniformity.
|
||||
if type(var_value) == "string" then
|
||||
var_value = { var_value }
|
||||
end
|
||||
|
||||
if
|
||||
(#var_value == 1 and #var_value[1] == 0)
|
||||
or #var_value == 0
|
||||
then
|
||||
-- var is empty, default is inserted.
|
||||
-- if no default, it's not interactive (an empty string is inserted).
|
||||
return default and default:is_interactive()
|
||||
end
|
||||
|
||||
-- variable is just inserted, not interactive.
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local d = dNode.D(ast.potential_tabstop, fn, {}, {
|
||||
-- TRICKY!!!!
|
||||
-- Problem: if the default is passed to the dynamicNode-function via lambda-capture, the
|
||||
-- copy-routine, which will run on expansion, cannot associate these
|
||||
-- nodes inside the passed nodes with the ones that are inside the
|
||||
-- snippet.
|
||||
-- For example, if `default` contains a functionNode which relies on
|
||||
-- an insertNode within the snippet, it has the insertNode as an
|
||||
-- argnode stored inside it. During copy, the copied insertNode (eg
|
||||
-- a pointer to it) has to be inserted at this position as well,
|
||||
-- otherwise there might be bugs (the snippet thinks the argnode is
|
||||
-- present, but it isn't).
|
||||
--
|
||||
-- This means that these nodes may not be passed as a simple
|
||||
-- lambda-capture (!!).
|
||||
-- I don't really like this, it can lead to very subtle errors (not
|
||||
-- in this instance, but needing to do this in general).
|
||||
--
|
||||
-- TODO: think about ways to avoid this. OTOH, this is almost okay,
|
||||
-- just needs to be documented a bit.
|
||||
--
|
||||
-- `default` is potentially nil.
|
||||
user_args = { default },
|
||||
})
|
||||
d.is_interactive = is_interactive_fn
|
||||
|
||||
-- if the variable is preceded by \n<indent>, the indent is applied to
|
||||
-- all lines of the variable (important for eg. TM_SELECTED_TEXT).
|
||||
if ast.previous_text ~= nil and #ast.previous_text > 1 then
|
||||
local last_line_indent =
|
||||
ast.previous_text[#ast.previous_text]:match("^%s+$")
|
||||
if last_line_indent then
|
||||
-- TM_SELECTED_TEXT contains the indent of the selected
|
||||
-- snippets, which leads to correct indentation if the
|
||||
-- snippet is expanded at the position the text was removed
|
||||
-- from.
|
||||
-- This seems pretty stupid, but TM_SELECTED_TEXT is
|
||||
-- desigend to be compatible with vscode.
|
||||
-- Use SELECT_DEDENT insted.
|
||||
-- stylua: ignore
|
||||
local indentstring = var ~= "TM_SELECTED_TEXT"
|
||||
and "$PARENT_INDENT" .. last_line_indent
|
||||
or last_line_indent
|
||||
|
||||
-- adjust current d's jump-position..
|
||||
d.pos = 1
|
||||
-- ..so it has the correct position when wrapped inside a
|
||||
-- snippetNode.
|
||||
d = sNode.ISN(ast.potential_tabstop, { d }, indentstring)
|
||||
end
|
||||
end
|
||||
|
||||
ast.parsed = d
|
||||
end,
|
||||
}
|
||||
|
||||
--- Converts any ast into luasnip-nodes.
|
||||
--- Snippets return a table of nodes, those can be used like the return-value of `fmt`.
|
||||
---@param ast table: AST, as generated by `require("vim.lsp._snippet").parse`
|
||||
---@param state table:
|
||||
--- - `var_functions`: table, string -> {dNode-fn, is_interactive_fn}
|
||||
--- For now, only used when parsing snipmate-snippets.
|
||||
---@return table: node corresponding to `ast`.
|
||||
function to_node(ast, state)
|
||||
if not Ast.is_node(ast) then
|
||||
-- ast is not an ast (probably a luasnip-node), return it as-is.
|
||||
return ast
|
||||
end
|
||||
return to_node_funcs[ast.type](ast, state)
|
||||
end
|
||||
|
||||
--- Converts any ast into usable nodes.
|
||||
---@param ast table: AST, as generated by `require("vim.lsp._snippet").parse`
|
||||
---@param state table:
|
||||
--- - `var_functions`: table, string -> {dNode-fn, is_interactive_fn}
|
||||
--- For now, only used when parsing snipmate-snippets.
|
||||
---@return table: list of luasnip-nodes.
|
||||
function M.to_luasnip_nodes(ast, state)
|
||||
state = state or {}
|
||||
state.var_functions = state.var_functions or {}
|
||||
|
||||
ast_utils.give_vars_potential_tabstop(ast)
|
||||
|
||||
-- fix disallowed $0 in snippet.
|
||||
-- TODO(logging): report changes here.
|
||||
ast_utils.fix_zero(ast)
|
||||
|
||||
-- Variables need the text just in front of them to determine whether to
|
||||
-- indent all lines of the Variable.
|
||||
ast_utils.give_vars_previous_text(ast)
|
||||
|
||||
local ast_nodes_topsort = ast_utils.parse_order(ast)
|
||||
assert(
|
||||
ast_nodes_topsort,
|
||||
"cannot represent snippet: contains circular dependencies"
|
||||
)
|
||||
for _, node in ipairs(ast_nodes_topsort) do
|
||||
to_node(node, state)
|
||||
end
|
||||
|
||||
return ast.parsed
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,461 @@
|
||||
local Ast = require("luasnip.util.parser.neovim_ast")
|
||||
local types = Ast.node_type
|
||||
local util = require("luasnip.util.util")
|
||||
local Str = require("luasnip.util.str")
|
||||
local log = require("luasnip.util.log").new("parser")
|
||||
local jsregexp_compile_safe = require("luasnip.util.jsregexp")
|
||||
|
||||
local directed_graph = require("luasnip.util.directed_graph")
|
||||
|
||||
local M = {}
|
||||
|
||||
---Walks ast pre-order, from left to right, applying predicate fn.
|
||||
---The walk is aborted as soon as fn matches (eg. returns true).
|
||||
---The walk does not recurse into Transform or choice, eg. it only covers nodes
|
||||
---that can be jumped (in)to.
|
||||
---@param ast table: the tree.
|
||||
---@param fn function: the predicate.
|
||||
---@return boolean: whether the predicate matched.
|
||||
local function predicate_ltr_nodes(ast, fn)
|
||||
if fn(ast) then
|
||||
return true
|
||||
end
|
||||
for _, node in ipairs(ast.children or {}) do
|
||||
if predicate_ltr_nodes(node, fn) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- tested in vscode:
|
||||
-- in "${1|b,c|} ${1:aa}" ${1:aa} is the copy,
|
||||
-- in "${1:aa}, ${1|b,c|}" ${1|b,c} is the copy => with these two the position
|
||||
-- determines which is the real tabstop => they have the same priority.
|
||||
-- in "$1 ${1:aa}", $1 is the copy, so it has to have a lower priority.
|
||||
local function type_real_tabstop_prio(node)
|
||||
local _type_real_tabstop_prio = {
|
||||
[types.TABSTOP] = 1,
|
||||
[types.PLACEHOLDER] = 2,
|
||||
[types.CHOICE] = 2,
|
||||
}
|
||||
if node.transform then
|
||||
return 0
|
||||
end
|
||||
return _type_real_tabstop_prio[node.type]
|
||||
end
|
||||
|
||||
---The name of this function is horrible, but I can't come up with something
|
||||
---more succinct.
|
||||
---The idea here is to find which of two nodes is "smaller" in a
|
||||
---"real-tabstop"-ordering relation on all the nodes of a snippet.
|
||||
---REQUIREMENT!!! The nodes have to be passed in the order they appear in in
|
||||
---the snippet, eg. prev_node has to appear earlier in the text (or be a parent
|
||||
---of) current_node.
|
||||
---@param prev_node table: the ast node earlier in the text.
|
||||
---@param current_node table: the other ast node.
|
||||
---@return boolean: true if prev_node is less than (according to the
|
||||
---"real-tabstop"-ordering described above and in the docstring of
|
||||
---`add_dependents`), false otherwise.
|
||||
local function real_tabstop_order_less(prev_node, current_node)
|
||||
local prio_prev = type_real_tabstop_prio(prev_node)
|
||||
local prio_current = type_real_tabstop_prio(current_node)
|
||||
-- if type-prio is the same, the one that appeared earlier is the real tabstop.
|
||||
return prio_prev == prio_current and false or prio_prev < prio_current
|
||||
end
|
||||
|
||||
---Find the real (eg. the one that is not a copy) $0.
|
||||
---@param ast table: ast
|
||||
---@return number, number, boolean: first, the type of the node with position 0, then
|
||||
--- the child of `ast` containing it and last whether the real $0 is copied.
|
||||
local function real_zero_node(ast)
|
||||
local real_zero = nil
|
||||
local real_zero_indx = nil
|
||||
local is_copied = false
|
||||
|
||||
local _search_zero
|
||||
_search_zero = function(node)
|
||||
local had_zero = false
|
||||
-- find placeholder/tabstop/choice with position 0
|
||||
if node.tabstop == 0 then
|
||||
if not real_zero then
|
||||
real_zero = node
|
||||
had_zero = true
|
||||
else
|
||||
if real_tabstop_order_less(real_zero, node) then
|
||||
-- node has a higher prio than the current real_zero.
|
||||
real_zero = node
|
||||
had_zero = true
|
||||
end
|
||||
-- we already encountered a zero-node, since i(0) cannot be
|
||||
-- copied this has to be reported to the caller.
|
||||
is_copied = true
|
||||
end
|
||||
end
|
||||
for indx, child in ipairs(node.children or {}) do
|
||||
local zn, _ = _search_zero(child)
|
||||
-- due to recursion, this will be called last in the loop of the
|
||||
-- outermost snippet.
|
||||
-- real_zero_indx will be the position of the child of snippet, in
|
||||
-- which the real $0 is located.
|
||||
if zn then
|
||||
real_zero_indx = indx
|
||||
had_zero = true
|
||||
end
|
||||
end
|
||||
|
||||
return had_zero
|
||||
end
|
||||
_search_zero(ast)
|
||||
|
||||
return real_zero, real_zero_indx, is_copied
|
||||
end
|
||||
|
||||
local function count_tabstop(ast, tabstop_indx)
|
||||
local count = 0
|
||||
|
||||
predicate_ltr_nodes(ast, function(node)
|
||||
if node.tabstop == tabstop_indx then
|
||||
count = count + 1
|
||||
end
|
||||
-- only stop once all nodes were looked at.
|
||||
return false
|
||||
end)
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
local function text_only_placeholder(placeholder)
|
||||
local only_text = true
|
||||
|
||||
predicate_ltr_nodes(placeholder, function(node)
|
||||
if node == placeholder then
|
||||
-- ignore placeholder.
|
||||
return false
|
||||
end
|
||||
if node.type ~= types.TEXT then
|
||||
only_text = false
|
||||
-- we found non-text, no need to search more.
|
||||
return true
|
||||
end
|
||||
end)
|
||||
|
||||
return only_text
|
||||
end
|
||||
|
||||
local function max_position(ast)
|
||||
local max = 0
|
||||
predicate_ltr_nodes(ast, function(node)
|
||||
local new_max = node.tabstop or 0
|
||||
if new_max > max then
|
||||
max = new_max
|
||||
end
|
||||
-- don't stop early.
|
||||
return false
|
||||
end)
|
||||
|
||||
return max
|
||||
end
|
||||
|
||||
local function replace_position(ast, p1, p2)
|
||||
predicate_ltr_nodes(ast, function(node)
|
||||
if node.tabstop == p1 then
|
||||
node.tabstop = p2
|
||||
end
|
||||
-- look at all nodes.
|
||||
return false
|
||||
end)
|
||||
end
|
||||
|
||||
function M.fix_zero(ast)
|
||||
local zn, ast_child_with_0_indx, is_copied = real_zero_node(ast)
|
||||
-- if zn exists, is a tabstop, an immediate child of `ast`, and does not
|
||||
-- have to be copied, the snippet can be accurately represented by luasnip.
|
||||
-- (also if zn just does not exist, ofc).
|
||||
--
|
||||
-- If the snippet can't be represented as-is, the ast needs to be modified
|
||||
-- as described below.
|
||||
if
|
||||
not zn
|
||||
or (
|
||||
zn
|
||||
and not is_copied
|
||||
and (zn.type == types.TABSTOP or (zn.type == types.PLACEHOLDER and text_only_placeholder(
|
||||
zn
|
||||
)))
|
||||
and ast.children[ast_child_with_0_indx] == zn
|
||||
)
|
||||
then
|
||||
return
|
||||
end
|
||||
|
||||
-- bad, a choice or placeholder is at position 0.
|
||||
-- replace all ${0:...} with ${n+1:...} (n highest position)
|
||||
-- max_position is at least 0, all's good.
|
||||
local max_pos = max_position(ast)
|
||||
replace_position(ast, 0, max_pos + 1)
|
||||
|
||||
-- insert $0 as a direct child to snippet, just behind the original $0/the
|
||||
-- node containing it.
|
||||
table.insert(ast.children, ast_child_with_0_indx + 1, Ast.tabstop(0))
|
||||
end
|
||||
|
||||
---This function identifies which tabstops/placeholder/choices are copies, and
|
||||
---which are "real tabstops"(/choices/placeholders). The real tabstops are
|
||||
---extended with a list of their dependents (tabstop.dependents), the copies
|
||||
---with their real tabstop (copy.copies)
|
||||
---
|
||||
---Rules for which node of any two nodes with the same tabstop-index is the
|
||||
---real tabstop:
|
||||
--- - if one is a tabstop and the other a placeholder/choice, the
|
||||
--- placeholder/choice is the real tabstop.
|
||||
--- - if they are both tabstop or both placeholder/choice, the one which
|
||||
--- appears earlier in the snippet is the real tabstop.
|
||||
--- (in "${1: ${1:lel}}" the outer ${1:...} appears earlier).
|
||||
---
|
||||
---@param ast table: the AST.
|
||||
function M.add_dependents(ast)
|
||||
-- all nodes that have a tabstop.
|
||||
-- map tabstop-index (number) -> node.
|
||||
local tabstops = {}
|
||||
|
||||
-- nodes which copy some tabstop.
|
||||
-- map tabstop-index (number) -> node[] (since there could be multiple copies of that one snippet).
|
||||
local copies = {}
|
||||
|
||||
predicate_ltr_nodes(ast, function(node)
|
||||
if not node.tabstop then
|
||||
-- not a tabstop-node -> continue.
|
||||
return false
|
||||
end
|
||||
|
||||
if not tabstops[node.tabstop] then
|
||||
tabstops[node.tabstop] = node
|
||||
-- continue, we want to find all dependencies.
|
||||
return false
|
||||
end
|
||||
if not copies[node.tabstop] then
|
||||
copies[node.tabstop] = {}
|
||||
end
|
||||
if real_tabstop_order_less(tabstops[node.tabstop], node) then
|
||||
table.insert(copies[node.tabstop], tabstops[node.tabstop])
|
||||
tabstops[node.tabstop] = node
|
||||
else
|
||||
table.insert(copies[node.tabstop], node)
|
||||
end
|
||||
-- continue.
|
||||
return false
|
||||
end)
|
||||
|
||||
-- associate real tabstop with its copies (by storing the copies in the real tabstop).
|
||||
for i, real_tabstop in pairs(tabstops) do
|
||||
real_tabstop.dependents = {}
|
||||
for _, copy in ipairs(copies[i] or {}) do
|
||||
table.insert(real_tabstop.dependents, copy)
|
||||
copy.copies = real_tabstop
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function apply_modifier(text, modifier)
|
||||
local mod_fn = Str.vscode_string_modifiers[modifier]
|
||||
if mod_fn then
|
||||
return mod_fn(text)
|
||||
else
|
||||
-- this can't really be reached, since only correct and available
|
||||
-- modifiers are parsed successfully
|
||||
-- (https://github.com/L3MON4D3/LuaSnip/blob/5fbebf6409f86bc4b7b699c2c80745e1ed190c16/lua/luasnip/util/parser/neovim_parser.lua#L239-L245).
|
||||
log.warn(
|
||||
"Tried to apply unknown modifier `%s` while parsing snippet, recovering by applying identity instead.",
|
||||
modifier
|
||||
)
|
||||
return text
|
||||
end
|
||||
end
|
||||
|
||||
local function apply_transform_format(nodes, captures)
|
||||
local transformed = ""
|
||||
for _, node in ipairs(nodes) do
|
||||
if node.type == types.TEXT then
|
||||
transformed = transformed .. node.esc
|
||||
else
|
||||
local capture = captures[node.capture_index]
|
||||
-- capture exists if it ..exists.. and is nonempty.
|
||||
if capture and #capture > 0 then
|
||||
if node.if_text then
|
||||
transformed = transformed .. node.if_text
|
||||
elseif node.modifier then
|
||||
transformed = transformed
|
||||
.. apply_modifier(capture, node.modifier)
|
||||
else
|
||||
transformed = transformed .. capture
|
||||
end
|
||||
else
|
||||
if node.else_text then
|
||||
transformed = transformed .. node.else_text
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return transformed
|
||||
end
|
||||
|
||||
function M.apply_transform(transform)
|
||||
if jsregexp_compile_safe then
|
||||
local reg_compiled, err =
|
||||
jsregexp_compile_safe(transform.pattern, transform.option)
|
||||
|
||||
if reg_compiled then
|
||||
-- can be passed to functionNode!
|
||||
return function(lines)
|
||||
-- luasnip expects+passes lines as list, but regex needs one string.
|
||||
lines = table.concat(lines, "\n")
|
||||
local matches = reg_compiled(lines)
|
||||
|
||||
local transformed = ""
|
||||
-- index one past the end of previous match.
|
||||
-- This is used to append unmatched characters to `transformed`, so
|
||||
-- it's initialized such that the first append is from 1.
|
||||
local prev_match_end = 0
|
||||
for _, match in ipairs(matches) do
|
||||
-- begin_ind and end_ind are inclusive.
|
||||
transformed = transformed
|
||||
.. lines:sub(prev_match_end + 1, match.begin_ind - 1)
|
||||
.. apply_transform_format(
|
||||
transform.format,
|
||||
match.groups
|
||||
)
|
||||
|
||||
-- end-inclusive
|
||||
prev_match_end = match.end_ind
|
||||
end
|
||||
transformed = transformed
|
||||
.. lines:sub(prev_match_end + 1, #lines)
|
||||
|
||||
return vim.split(transformed, "\n")
|
||||
end
|
||||
else
|
||||
log.error(
|
||||
"Failed parsing regex `%s` with options `%s`: %s",
|
||||
transform.pattern,
|
||||
transform.option,
|
||||
err
|
||||
)
|
||||
-- fall through to returning identity.
|
||||
end
|
||||
end
|
||||
|
||||
-- without jsregexp, or without a valid regex, we cannot properly transform
|
||||
-- whatever is supposed to be transformed here.
|
||||
-- Just return a function that returns the to-be-transformed string
|
||||
-- unmodified.
|
||||
return util.id
|
||||
end
|
||||
|
||||
---Variables need the text which is in front of them to determine whether they
|
||||
---have to be indented ("asdf\n\t$TM_SELECTED_TEXT": vscode indents all lines
|
||||
---of TM_SELECTED_TEXT).
|
||||
---
|
||||
---The text is accessible as ast_node.previous_text, a string[].
|
||||
---@param ast table: the AST.
|
||||
function M.give_vars_previous_text(ast)
|
||||
local last_text = { "" }
|
||||
-- important: predicate_ltr_nodes visits the node in the order they appear,
|
||||
-- textually, in the snippet.
|
||||
-- This is necessary to actually ensure the variables actually get the text just in front of them.
|
||||
predicate_ltr_nodes(ast, function(node)
|
||||
if node.children then
|
||||
-- continue if this node is not a leaf.
|
||||
-- Since predicate_ltr_nodes runs fn first for the placeholder, and
|
||||
-- then for its' children, `last_text` would be reset wrongfully
|
||||
-- (example: "asdf\n\t${1:$TM_SELECTED_TEXT}". Here the placeholder
|
||||
-- is encountered before the variable -> no indentation).
|
||||
--
|
||||
-- ignoring non-leaf-nodes makes it so that only the nodes which
|
||||
-- actually contribute text (placeholders are "invisible" in that
|
||||
-- they don't add text themselves, they do it through their
|
||||
-- children) are considered.
|
||||
return false
|
||||
end
|
||||
if node.type == types.TEXT then
|
||||
last_text = vim.split(node.esc, "\n")
|
||||
elseif node.type == types.VARIABLE then
|
||||
node.previous_text = last_text
|
||||
else
|
||||
-- reset last_text when a different node is encountered.
|
||||
last_text = { "" }
|
||||
end
|
||||
-- continue..
|
||||
return false
|
||||
end)
|
||||
end
|
||||
|
||||
---Variables are turned into placeholders if the Variable is undefined or not set.
|
||||
---Since in luasnip, variables can be added at runtime, the decision whether a
|
||||
---variable is just some text, inserts its default, or its variable-name has to
|
||||
---be deferred to runtime.
|
||||
---So, each variable is a dynamicNode, and needs a tabstop.
|
||||
---In vscode the variables are visited
|
||||
--- 1) after all other tabstops/placeholders/choices and
|
||||
--- 2) in the order they appear in the snippet-body.
|
||||
---We mimic this behaviour.
|
||||
---@param ast table: The AST.
|
||||
function M.give_vars_potential_tabstop(ast)
|
||||
local last_tabstop = max_position(ast)
|
||||
|
||||
predicate_ltr_nodes(ast, function(node)
|
||||
if node.type == types.VARIABLE then
|
||||
last_tabstop = last_tabstop + 1
|
||||
node.potential_tabstop = last_tabstop
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function M.parse_order(ast)
|
||||
M.add_dependents(ast)
|
||||
-- build Directed Graph from ast-nodes.
|
||||
-- vertices are ast-nodes, edges define has-to-be-parsed-before-relations
|
||||
-- (a child of some placeholder would have an edge to it, real tabstops
|
||||
-- have edges to their copies).
|
||||
local g = directed_graph.new()
|
||||
-- map node -> vertex.
|
||||
local to_vert = {}
|
||||
|
||||
-- add one vertex for each node + create map node->vert.
|
||||
predicate_ltr_nodes(ast, function(node)
|
||||
to_vert[node] = g:add_vertex()
|
||||
end)
|
||||
|
||||
predicate_ltr_nodes(ast, function(node)
|
||||
if node.dependents then
|
||||
-- if the node has dependents, it has to be parsed before they are.
|
||||
for _, dep in ipairs(node.dependents) do
|
||||
g:set_edge(to_vert[node], to_vert[dep])
|
||||
end
|
||||
end
|
||||
if node.children then
|
||||
-- if the node has children, they have to be parsed before it can
|
||||
-- be parsed.
|
||||
for _, child in ipairs(node.children) do
|
||||
g:set_edge(to_vert[child], to_vert[node])
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local topsort = g:topological_sort()
|
||||
if not topsort then
|
||||
-- ast (with additional dependencies) contains circle.
|
||||
return nil
|
||||
end
|
||||
|
||||
local to_node = util.reverse_lookup(to_vert)
|
||||
return vim.tbl_map(function(vertex)
|
||||
return to_node[vertex]
|
||||
end, topsort)
|
||||
end
|
||||
|
||||
M.types = types
|
||||
|
||||
return M
|
||||
@ -0,0 +1,141 @@
|
||||
local sNode = require("luasnip.nodes.snippet")
|
||||
local ast_parser = require("luasnip.util.parser.ast_parser")
|
||||
local parse = require("luasnip.util.parser.neovim_parser").parse
|
||||
local Ast = require("luasnip.util.parser.neovim_ast")
|
||||
local Str = require("luasnip.util.str")
|
||||
local functions = require("luasnip.util.functions")
|
||||
local util = require("luasnip.util.util")
|
||||
local extend_decorator = require("luasnip.util.extend_decorator")
|
||||
|
||||
local M = {}
|
||||
|
||||
---Parse snippet represented by `body`.
|
||||
---@param context (table|string|number|nil):
|
||||
--- - table|string: treated like the first argument to `ls.snippet`,
|
||||
--- returns a snippet.
|
||||
--- - number: Returns a snippetNode, `context` is its' jump-position.
|
||||
--- - nil: Returns a flat list of luasnip-nodes, to be used however.
|
||||
---@param body string: the representation of the snippet.
|
||||
---@param opts table|nil: optional parameters. Valid keys:
|
||||
--- - `trim_empty`: boolean, remove empty lines from the snippet.
|
||||
--- - `dedent`: boolean, remove common indent from the snippet's lines.
|
||||
--- - `variables`: map[string-> (fn()->string)], variables to be used only in this
|
||||
--- snippet.
|
||||
---@return table: the snippet, in the representation dictated by the value of
|
||||
---`context`.
|
||||
function M.parse_snippet(context, body, opts)
|
||||
opts = opts or {}
|
||||
if opts.dedent == nil then
|
||||
opts.dedent = true
|
||||
end
|
||||
if opts.trim_empty == nil then
|
||||
opts.trim_empty = true
|
||||
end
|
||||
|
||||
body = Str.sanitize(body)
|
||||
|
||||
local lines = vim.split(body, "\n")
|
||||
Str.process_multiline(lines, opts)
|
||||
body = table.concat(lines, "\n")
|
||||
|
||||
local ast
|
||||
if body == "" then
|
||||
ast = Ast.snippet({
|
||||
Ast.text(""),
|
||||
})
|
||||
else
|
||||
ast = parse(body)
|
||||
end
|
||||
|
||||
local nodes = ast_parser.to_luasnip_nodes(ast, {
|
||||
var_functions = opts.variables,
|
||||
})
|
||||
|
||||
if type(context) == "number" then
|
||||
return sNode.SN(context, nodes)
|
||||
end
|
||||
if type(context) == "nil" then
|
||||
return nodes
|
||||
end
|
||||
|
||||
if type(context) == "string" then
|
||||
context = { trig = context }
|
||||
end
|
||||
context.docstring = body
|
||||
|
||||
return sNode.S(context, nodes)
|
||||
end
|
||||
local function context_extend(arg, extend)
|
||||
local argtype = type(arg)
|
||||
if argtype == "string" then
|
||||
arg = { trig = arg }
|
||||
end
|
||||
|
||||
if argtype == "table" then
|
||||
return vim.tbl_extend("keep", arg, extend or {})
|
||||
end
|
||||
|
||||
-- fall back to unchanged arg.
|
||||
-- log this, probably.
|
||||
return arg
|
||||
end
|
||||
extend_decorator.register(
|
||||
M.parse_snippet,
|
||||
{ arg_indx = 1, extend = context_extend },
|
||||
{ arg_indx = 3 }
|
||||
)
|
||||
|
||||
local function backticks_to_variable(body)
|
||||
local var_map = {}
|
||||
local variable_indx = 1
|
||||
local var_string = ""
|
||||
|
||||
local processed_to = 1
|
||||
for from, to in Str.unescaped_pairs(body, "`", "`") do
|
||||
local varname = "LUASNIP_SNIPMATE_VAR" .. variable_indx
|
||||
var_string = var_string
|
||||
-- since the first unescaped ` is at from, there is no unescaped `
|
||||
-- in body:sub(old_to, from-1). We can therefore gsub occurences of
|
||||
-- \`, without worrying about potentially changing something like
|
||||
-- \\` (or \\\\`) into \` (\\\`).
|
||||
.. body:sub(processed_to, from - 1):gsub("\\`", "`")
|
||||
-- `$varname` is unsafe, might lead to something like "my
|
||||
-- snip$LUASNIP_SNIPMATE_VAR1pet", where the variable is
|
||||
-- interpreted as "LUASNIP_SNIPMATE_VAR1pet".
|
||||
-- This cannot happen with curly braces.
|
||||
.. "${"
|
||||
.. varname
|
||||
.. "}"
|
||||
|
||||
-- don't include backticks in vimscript.
|
||||
var_map[varname] =
|
||||
functions.eval_vim_dynamic(body:sub(from + 1, to - 1))
|
||||
processed_to = to + 1
|
||||
variable_indx = variable_indx + 1
|
||||
end
|
||||
|
||||
-- append remaining characters.
|
||||
var_string = var_string .. body:sub(processed_to, -1):gsub("\\`", "`")
|
||||
|
||||
return var_map, var_string
|
||||
end
|
||||
|
||||
function M.parse_snipmate(context, body, opts)
|
||||
local new_vars
|
||||
new_vars, body = backticks_to_variable(body)
|
||||
|
||||
opts = opts or {}
|
||||
opts.variables = {}
|
||||
for name, fn in pairs(new_vars) do
|
||||
-- created dynamicNode is not interactive.
|
||||
opts.variables[name] = { fn, util.no }
|
||||
end
|
||||
return M.parse_snippet(context, body, opts)
|
||||
end
|
||||
extend_decorator.register(
|
||||
M.parse_snipmate,
|
||||
{ arg_indx = 1, extend = context_extend },
|
||||
{ arg_indx = 3 }
|
||||
)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,207 @@
|
||||
-- ripped out of neovim.
|
||||
|
||||
local M = {}
|
||||
|
||||
local node_type = {
|
||||
SNIPPET = 0,
|
||||
TABSTOP = 1,
|
||||
PLACEHOLDER = 2,
|
||||
VARIABLE = 3,
|
||||
CHOICE = 4,
|
||||
TRANSFORM = 5,
|
||||
FORMAT = 6,
|
||||
TEXT = 7,
|
||||
}
|
||||
|
||||
M.node_type = node_type
|
||||
|
||||
local Node = {}
|
||||
function Node:__tostring()
|
||||
local insert_text = {}
|
||||
if self.type == node_type.SNIPPET then
|
||||
for _, c in ipairs(self.children) do
|
||||
table.insert(insert_text, tostring(c))
|
||||
end
|
||||
elseif self.type == node_type.CHOICE then
|
||||
table.insert(insert_text, self.items[1])
|
||||
elseif self.type == node_type.PLACEHOLDER then
|
||||
for _, c in ipairs(self.children or {}) do
|
||||
table.insert(insert_text, tostring(c))
|
||||
end
|
||||
elseif self.type == node_type.TEXT then
|
||||
table.insert(insert_text, self.esc)
|
||||
end
|
||||
return table.concat(insert_text, "")
|
||||
end
|
||||
|
||||
--- @private
|
||||
local function new(t)
|
||||
return setmetatable(t, Node)
|
||||
end
|
||||
|
||||
---Determine whether {t} is an AST-node.
|
||||
---@param t table
|
||||
---@return boolean
|
||||
local function is_node(t)
|
||||
return getmetatable(t) == Node
|
||||
end
|
||||
|
||||
M.is_node = is_node
|
||||
|
||||
---Create a new snippet.
|
||||
---@param children (ast-node[]) Contents of the snippet.
|
||||
---@return table |lsp-parser-snippet|
|
||||
function M.snippet(children)
|
||||
return new({
|
||||
type = node_type.SNIPPET,
|
||||
children = children,
|
||||
})
|
||||
end
|
||||
|
||||
---Create a new tabstop.
|
||||
---@param tabstop (number) Position of this tabstop.
|
||||
---@param transform (table|nil) optional transform applied to the tabstop.
|
||||
---@return table |lsp-parser-tabstop|
|
||||
function M.tabstop(tabstop, transform)
|
||||
return new({
|
||||
type = node_type.TABSTOP,
|
||||
tabstop = tabstop,
|
||||
transform = transform,
|
||||
})
|
||||
end
|
||||
|
||||
---Create a new placeholder.
|
||||
---@param tabstop (number) Position of the placeholder.
|
||||
---@param children (ast-node[]) Content of the placeholder.
|
||||
---@return table |lsp-parser-placeholder|
|
||||
function M.placeholder(tabstop, children)
|
||||
return new({
|
||||
type = node_type.PLACEHOLDER,
|
||||
tabstop = tabstop,
|
||||
children = children,
|
||||
})
|
||||
end
|
||||
|
||||
---Create a new variable.
|
||||
---@param name (string) Name.
|
||||
---@param replacement (node[] | transform | nil)
|
||||
--- - (node[]) Inserted when the variable is empty.
|
||||
--- - (transform) Applied to the variable's value.
|
||||
---@return table |lsp-parser-variable|
|
||||
function M.variable(name, replacement)
|
||||
local transform, children
|
||||
-- transform is an ast-node, children a flat list of nodes.
|
||||
if is_node(replacement) then
|
||||
transform = replacement
|
||||
else
|
||||
children = replacement
|
||||
end
|
||||
|
||||
return new({
|
||||
type = node_type.VARIABLE,
|
||||
name = name,
|
||||
transform = transform,
|
||||
children = children,
|
||||
})
|
||||
end
|
||||
|
||||
---Create a new choice.
|
||||
---@param tabstop (number) Position of the choice.
|
||||
---@param items (string[]) Choices.
|
||||
---@return table |lsp-parser-choice|
|
||||
function M.choice(tabstop, items)
|
||||
return new({
|
||||
type = node_type.CHOICE,
|
||||
tabstop = tabstop,
|
||||
items = items,
|
||||
})
|
||||
end
|
||||
|
||||
---Create a new transform.
|
||||
---@param pattern (string) Regex applied to the variable/tabstop this transform
|
||||
--- is supplied to.
|
||||
---@param format (table of Format|Text) Replacement for the regex.
|
||||
---@param option (string|nil) Regex-options, default "".
|
||||
---@return table |lsp-parser-transform|
|
||||
function M.transform(pattern, format, option)
|
||||
return new({
|
||||
type = node_type.TRANSFORM,
|
||||
pattern = pattern,
|
||||
format = format,
|
||||
option = option or "",
|
||||
})
|
||||
end
|
||||
|
||||
---Create a new format which either inserts the capture at {capture_index},
|
||||
---applies a modifier to the capture or inserts {if_text} if the capture is
|
||||
---nonempty, and {else_text} otherwise.
|
||||
---@param capture_index (number) Capture this format is applied to.
|
||||
---@param capture_transform (string | table | nil)
|
||||
--- - (string): {capture_transform} is a modifier.
|
||||
--- - (table): {capture_transform} can contain either of
|
||||
--- - {if_text} (string) Inserted for nonempty
|
||||
--- capture.
|
||||
--- - {else_text} (string) Inserted for empty or
|
||||
--- undefined capture.
|
||||
---@return table |lsp-parser-format|
|
||||
function M.format(capture_index, capture_transform)
|
||||
local if_text, else_text, modifier
|
||||
if type(capture_transform) == "table" then
|
||||
if_text = capture_transform.if_text
|
||||
else_text = capture_transform.else_text
|
||||
elseif type(capture_transform) == "string" then
|
||||
modifier = capture_transform
|
||||
end
|
||||
|
||||
return new({
|
||||
type = node_type.FORMAT,
|
||||
capture_index = capture_index,
|
||||
modifier = modifier,
|
||||
if_text = if_text,
|
||||
else_text = else_text,
|
||||
})
|
||||
end
|
||||
|
||||
---Create new text.
|
||||
---@param esc (string) Escaped text.
|
||||
---@param raw (string|nil, default {esc}) Unescaped text.
|
||||
---
|
||||
---@return table |lsp-parser-text|
|
||||
function M.text(esc, raw)
|
||||
return new({
|
||||
type = node_type.TEXT,
|
||||
esc = esc,
|
||||
raw = raw or esc,
|
||||
})
|
||||
end
|
||||
|
||||
function M.merge_adjacent_text(ast)
|
||||
if ast.children then
|
||||
-- build new table of children.
|
||||
local new_children = {}
|
||||
-- last_child shall always point to the last entry in new_children.
|
||||
local last_child
|
||||
|
||||
for _, child in ipairs(ast.children) do
|
||||
-- first, recurse into children.
|
||||
-- When we do this is not important, since it does not change the TEXT-nodes, here is just comfortable.
|
||||
M.merge_adjacent_text(child)
|
||||
|
||||
if
|
||||
child.type == node_type.TEXT
|
||||
and last_child
|
||||
and last_child.type == node_type.TEXT
|
||||
then
|
||||
last_child.raw = last_child.raw .. child.raw
|
||||
last_child.esc = last_child.esc .. child.esc
|
||||
else
|
||||
table.insert(new_children, child)
|
||||
last_child = child
|
||||
end
|
||||
end
|
||||
|
||||
ast.children = new_children
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,479 @@
|
||||
-- ripped out of neovim.
|
||||
|
||||
local P = {}
|
||||
|
||||
local ast = require("luasnip.util.parser.neovim_ast")
|
||||
|
||||
---Take characters until the target characters (The escape sequence is '\' + char)
|
||||
---@param targets string[] The character list for stop consuming text.
|
||||
---@param specials string[] If the character isn't contained in targets/specials, '\' will be left.
|
||||
---@private
|
||||
function P.take_until(targets, specials)
|
||||
targets = targets or {}
|
||||
specials = specials or {}
|
||||
|
||||
return function(input, pos)
|
||||
local new_pos = pos
|
||||
local raw = {}
|
||||
local esc = {}
|
||||
while new_pos <= #input do
|
||||
local c = string.sub(input, new_pos, new_pos)
|
||||
if c == "\\" then
|
||||
table.insert(raw, "\\")
|
||||
new_pos = new_pos + 1
|
||||
c = string.sub(input, new_pos, new_pos)
|
||||
if
|
||||
not vim.tbl_contains(targets, c)
|
||||
and not vim.tbl_contains(specials, c)
|
||||
then
|
||||
table.insert(esc, "\\")
|
||||
end
|
||||
table.insert(raw, c)
|
||||
table.insert(esc, c)
|
||||
new_pos = new_pos + 1
|
||||
else
|
||||
if vim.tbl_contains(targets, c) then
|
||||
break
|
||||
end
|
||||
table.insert(raw, c)
|
||||
table.insert(esc, c)
|
||||
new_pos = new_pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if new_pos == pos then
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
|
||||
return {
|
||||
parsed = true,
|
||||
value = {
|
||||
raw = table.concat(raw, ""),
|
||||
esc = table.concat(esc, ""),
|
||||
},
|
||||
pos = new_pos,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function P.unmatch(pos)
|
||||
return {
|
||||
parsed = false,
|
||||
value = nil,
|
||||
pos = pos,
|
||||
}
|
||||
end
|
||||
|
||||
---@private
|
||||
function P.map(parser, map)
|
||||
return function(input, pos)
|
||||
local result = parser(input, pos)
|
||||
if result.parsed then
|
||||
return {
|
||||
parsed = true,
|
||||
value = map(result.value),
|
||||
pos = result.pos,
|
||||
}
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function P.lazy(factory)
|
||||
return function(input, pos)
|
||||
return factory()(input, pos)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function P.token(token)
|
||||
return function(input, pos)
|
||||
local maybe_token = string.sub(input, pos, pos + #token - 1)
|
||||
if token == maybe_token then
|
||||
return {
|
||||
parsed = true,
|
||||
value = maybe_token,
|
||||
pos = pos + #token,
|
||||
}
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function P.pattern(p)
|
||||
return function(input, pos)
|
||||
local maybe_match = string.match(string.sub(input, pos), "^" .. p)
|
||||
if maybe_match then
|
||||
return {
|
||||
parsed = true,
|
||||
value = maybe_match,
|
||||
pos = pos + #maybe_match,
|
||||
}
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function P.many(parser)
|
||||
return function(input, pos)
|
||||
local values = {}
|
||||
local new_pos = pos
|
||||
while new_pos <= #input do
|
||||
local result = parser(input, new_pos)
|
||||
if not result.parsed then
|
||||
break
|
||||
end
|
||||
table.insert(values, result.value)
|
||||
new_pos = result.pos
|
||||
end
|
||||
if #values > 0 then
|
||||
return {
|
||||
parsed = true,
|
||||
value = values,
|
||||
pos = new_pos,
|
||||
}
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function P.any(...)
|
||||
local parsers = { ... }
|
||||
return function(input, pos)
|
||||
for _, parser in ipairs(parsers) do
|
||||
local result = parser(input, pos)
|
||||
if result.parsed then
|
||||
return result
|
||||
end
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function P.opt(parser)
|
||||
return function(input, pos)
|
||||
local result = parser(input, pos)
|
||||
return {
|
||||
parsed = true,
|
||||
value = result.value,
|
||||
pos = result.pos,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function P.seq(...)
|
||||
local parsers = { ... }
|
||||
return function(input, pos)
|
||||
local values = {}
|
||||
local new_pos = pos
|
||||
for i, parser in ipairs(parsers) do
|
||||
local result = parser(input, new_pos)
|
||||
if result.parsed then
|
||||
values[i] = result.value
|
||||
new_pos = result.pos
|
||||
else
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
return {
|
||||
parsed = true,
|
||||
value = values,
|
||||
pos = new_pos,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
|
||||
|
||||
local S = {}
|
||||
S.dollar = P.token("$")
|
||||
S.open = P.token("{")
|
||||
S.close = P.token("}")
|
||||
S.colon = P.token(":")
|
||||
S.slash = P.token("/")
|
||||
S.comma = P.token(",")
|
||||
S.pipe = P.token("|")
|
||||
S.plus = P.token("+")
|
||||
S.minus = P.token("-")
|
||||
S.question = P.token("?")
|
||||
S.int = P.map(P.pattern("[0-9]+"), function(value)
|
||||
return tonumber(value, 10)
|
||||
end)
|
||||
S.var = P.pattern("[%a_][%w_]+")
|
||||
S.text = function(targets, specials)
|
||||
return P.map(P.take_until(targets, specials), function(value)
|
||||
return ast.text(value.esc, value.raw)
|
||||
end)
|
||||
end
|
||||
S.patterntext = function(pattern)
|
||||
return P.map(P.pattern(pattern), function(value)
|
||||
return ast.text(value, value)
|
||||
end)
|
||||
end
|
||||
S.text_or_empty = function(targets, specials)
|
||||
return P.map(
|
||||
P.any(P.take_until(targets, specials), P.token("")),
|
||||
function(value)
|
||||
-- if it is empty, we have to return a valid S.text-object.
|
||||
if value == "" then
|
||||
return ast.text("", "")
|
||||
else
|
||||
return ast.text(value.esc, value.raw)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
S.toplevel = P.lazy(function()
|
||||
return P.any(S.placeholder, S.tabstop, S.variable, S.choice)
|
||||
end)
|
||||
|
||||
S.format = P.any(
|
||||
P.map(P.seq(S.dollar, S.int), function(values)
|
||||
return ast.format(values[2])
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
|
||||
return ast.format(values[3])
|
||||
end),
|
||||
P.map(
|
||||
P.seq(
|
||||
S.dollar,
|
||||
S.open,
|
||||
S.int,
|
||||
S.colon,
|
||||
S.slash,
|
||||
P.any(
|
||||
P.token("upcase"),
|
||||
P.token("downcase"),
|
||||
P.token("capitalize"),
|
||||
P.token("camelcase"),
|
||||
P.token("pascalcase")
|
||||
),
|
||||
S.close
|
||||
),
|
||||
function(values)
|
||||
return ast.format(values[3], values[6])
|
||||
end
|
||||
),
|
||||
P.map(
|
||||
P.seq(
|
||||
S.dollar,
|
||||
S.open,
|
||||
S.int,
|
||||
S.colon,
|
||||
P.seq(
|
||||
S.question,
|
||||
P.opt(P.take_until({ ":" }, { "\\" })),
|
||||
S.colon,
|
||||
P.opt(P.take_until({ "}" }, { "\\" }))
|
||||
),
|
||||
S.close
|
||||
),
|
||||
function(values)
|
||||
return ast.format(values[3], {
|
||||
if_text = values[5][2] and values[5][2].esc or "",
|
||||
else_text = values[5][4] and values[5][4].esc or "",
|
||||
})
|
||||
end
|
||||
),
|
||||
P.map(
|
||||
P.seq(
|
||||
S.dollar,
|
||||
S.open,
|
||||
S.int,
|
||||
S.colon,
|
||||
P.seq(S.plus, P.opt(P.take_until({ "}" }, { "\\" }))),
|
||||
S.close
|
||||
),
|
||||
function(values)
|
||||
return ast.format(values[3], {
|
||||
if_text = values[5][2] and values[5][2].esc or "",
|
||||
})
|
||||
end
|
||||
),
|
||||
P.map(
|
||||
P.seq(
|
||||
S.dollar,
|
||||
S.open,
|
||||
S.int,
|
||||
S.colon,
|
||||
S.minus,
|
||||
P.opt(P.take_until({ "}" }, { "\\" })),
|
||||
S.close
|
||||
),
|
||||
function(values)
|
||||
return ast.format(values[3], {
|
||||
else_text = values[6] and values[6].esc or "",
|
||||
})
|
||||
end
|
||||
),
|
||||
P.map(
|
||||
P.seq(
|
||||
S.dollar,
|
||||
S.open,
|
||||
S.int,
|
||||
S.colon,
|
||||
P.opt(P.take_until({ "}" }, { "\\" })),
|
||||
S.close
|
||||
),
|
||||
function(values)
|
||||
return ast.format(values[3], {
|
||||
else_text = values[5] and values[5].esc or "",
|
||||
})
|
||||
end
|
||||
)
|
||||
)
|
||||
|
||||
S.transform = P.map(
|
||||
P.seq(
|
||||
S.slash,
|
||||
P.take_until({ "/" }, { "\\" }),
|
||||
S.slash,
|
||||
P.many(
|
||||
P.any(
|
||||
S.format,
|
||||
S.text({ "$", "/" }, { "\\" }),
|
||||
S.patterntext("[^/]")
|
||||
)
|
||||
),
|
||||
S.slash,
|
||||
P.opt(P.pattern("[ig]+"))
|
||||
),
|
||||
function(values)
|
||||
return ast.transform(values[2].raw, values[4], values[6])
|
||||
end
|
||||
)
|
||||
|
||||
S.tabstop = P.any(
|
||||
P.map(P.seq(S.dollar, S.int), function(values)
|
||||
return ast.tabstop(values[2])
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
|
||||
return ast.tabstop(values[3])
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.transform, S.close), function(values)
|
||||
return ast.tabstop(values[3], values[4])
|
||||
end)
|
||||
)
|
||||
|
||||
S.placeholder = P.any(
|
||||
P.map(
|
||||
P.seq(
|
||||
S.dollar,
|
||||
S.open,
|
||||
S.int,
|
||||
S.colon,
|
||||
P.opt(
|
||||
P.many(
|
||||
P.any(
|
||||
S.toplevel,
|
||||
S.text({ "$", "}" }, { "\\" }),
|
||||
S.patterntext("[^}]")
|
||||
)
|
||||
)
|
||||
),
|
||||
S.close
|
||||
),
|
||||
function(values)
|
||||
-- no children -> manually create empty text.
|
||||
return ast.placeholder(values[3], values[5] or { ast.text("") })
|
||||
end
|
||||
)
|
||||
)
|
||||
|
||||
S.choice = P.map(
|
||||
P.seq(
|
||||
S.dollar,
|
||||
S.open,
|
||||
S.int,
|
||||
S.pipe,
|
||||
P.opt(
|
||||
P.many(
|
||||
P.map(
|
||||
P.seq(S.text_or_empty({ ",", "|" }), S.comma),
|
||||
function(values)
|
||||
return values[1].esc
|
||||
end
|
||||
)
|
||||
)
|
||||
),
|
||||
P.map(P.any(S.text_or_empty({ ",", "|" }), S.empty), function(values)
|
||||
return values.esc
|
||||
end),
|
||||
S.pipe,
|
||||
S.close
|
||||
),
|
||||
function(values)
|
||||
local choices = values[5] or {}
|
||||
table.insert(choices, values[6])
|
||||
return ast.choice(values[3], choices)
|
||||
end
|
||||
)
|
||||
|
||||
S.variable = P.any(
|
||||
P.map(P.seq(S.dollar, S.var), function(values)
|
||||
return ast.variable(values[2])
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.var, S.close), function(values)
|
||||
return ast.variable(values[3])
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.var, S.transform, S.close), function(values)
|
||||
return ast.variable(values[3], values[4])
|
||||
end),
|
||||
P.map(
|
||||
P.seq(
|
||||
S.dollar,
|
||||
S.open,
|
||||
S.var,
|
||||
S.colon,
|
||||
P.many(
|
||||
P.any(
|
||||
S.toplevel,
|
||||
S.text({ "$", "}" }, { "\\" }),
|
||||
S.patterntext("[^}]")
|
||||
)
|
||||
),
|
||||
S.close
|
||||
),
|
||||
function(values)
|
||||
return ast.variable(values[3], values[5])
|
||||
end
|
||||
)
|
||||
)
|
||||
|
||||
S.snippet = P.map(
|
||||
P.many(
|
||||
P.any(S.toplevel, S.text({ "$" }, { "}", "\\" }), S.patterntext("."))
|
||||
),
|
||||
function(values)
|
||||
-- Insp(values)
|
||||
return ast.snippet(values)
|
||||
end
|
||||
)
|
||||
|
||||
local M = {}
|
||||
|
||||
---Build the AST for {input}.
|
||||
---@param input string A snippet as defined in
|
||||
--- https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
|
||||
---@return (Snippet)
|
||||
function M.parse(input)
|
||||
local result = S.snippet(input, 1)
|
||||
if not result.parsed then
|
||||
error("snippet parsing failed.")
|
||||
end
|
||||
|
||||
ast.merge_adjacent_text(result.value)
|
||||
return result.value
|
||||
end
|
||||
|
||||
M.ast = ast
|
||||
|
||||
return M
|
||||
@ -0,0 +1,266 @@
|
||||
local Path = {}
|
||||
|
||||
local uv = vim.loop
|
||||
|
||||
local sep = (function()
|
||||
if jit then
|
||||
local os = jit.os:lower()
|
||||
if vim.tbl_contains({ "linux", "osx", "bsd" }, os) then
|
||||
return "/"
|
||||
else
|
||||
return "\\"
|
||||
end
|
||||
end
|
||||
return package.config:sub(1, 1)
|
||||
end)()
|
||||
|
||||
local root_pattern = (function()
|
||||
return uv.os_uname().sysname:find("Windows") and "%w%:" or "%/"
|
||||
end)()
|
||||
|
||||
function Path.join(...)
|
||||
return table.concat({ ... }, sep)
|
||||
end
|
||||
|
||||
function Path.exists(filepath)
|
||||
return uv.fs_stat(filepath) and true or false
|
||||
end
|
||||
|
||||
function Path.async_read_file(path, callback)
|
||||
uv.fs_open(path, "r", tonumber("0666", 8), function(err, fd)
|
||||
assert(not err, err)
|
||||
uv.fs_fstat(fd, function(err, stat)
|
||||
assert(not err, err)
|
||||
uv.fs_read(fd, stat.size, 0, function(err, buffer)
|
||||
assert(not err, err)
|
||||
uv.fs_close(fd, function(err)
|
||||
assert(not err, err)
|
||||
callback(buffer)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return string buffer @content of file
|
||||
function Path.read_file(path)
|
||||
-- permissions: rrr
|
||||
local fd = assert(uv.fs_open(path, "r", tonumber("0444", 8)))
|
||||
local stat = assert(uv.fs_fstat(fd))
|
||||
-- read from offset 0.
|
||||
local buf = assert(uv.fs_read(fd, stat.size, 0))
|
||||
uv.fs_close(fd)
|
||||
|
||||
return buf
|
||||
end
|
||||
|
||||
local MYCONFIG_ROOT
|
||||
|
||||
if vim.env.MYVIMRC then
|
||||
MYCONFIG_ROOT = vim.fn.fnamemodify(vim.env.MYVIMRC, ":p:h")
|
||||
else
|
||||
MYCONFIG_ROOT = vim.fn.getcwd()
|
||||
end
|
||||
|
||||
-- sometimes we don't want to resolve symlinks, but handle ~/ and ./
|
||||
function Path.expand_keep_symlink(filepath)
|
||||
-- omit second return-value of :gsub
|
||||
local res = filepath
|
||||
:gsub("^~", vim.env.HOME)
|
||||
:gsub("^[.][/\\]", MYCONFIG_ROOT .. sep)
|
||||
return res
|
||||
end
|
||||
function Path.expand(filepath)
|
||||
return uv.fs_realpath(Path.expand_keep_symlink(filepath))
|
||||
end
|
||||
|
||||
-- do our best at normalizing a non-existing path.
|
||||
function Path.normalize_nonexisting(filepath, cwd)
|
||||
cwd = cwd or vim.fn.getcwd()
|
||||
|
||||
local normalized = filepath
|
||||
-- replace multiple slashes by one.
|
||||
:gsub(sep .. sep .. "+", sep)
|
||||
-- remove trailing slash.
|
||||
:gsub(sep .. "$", "")
|
||||
-- remove ./ from path.
|
||||
:gsub("%." .. sep, "")
|
||||
|
||||
-- if not yet absolute, prepend path to current directory.
|
||||
if not normalized:match("^" .. root_pattern .. "") then
|
||||
normalized = Path.join(cwd, normalized)
|
||||
end
|
||||
|
||||
return normalized
|
||||
end
|
||||
|
||||
function Path.expand_nonexisting(filepath, cwd)
|
||||
filepath
|
||||
-- replace ~ with home-directory.
|
||||
:gsub("^~", vim.env.HOME)
|
||||
-- replace ./ or .\ with config-directory (likely ~/.config/nvim)
|
||||
:gsub(
|
||||
"^[.][/\\]",
|
||||
MYCONFIG_ROOT .. sep
|
||||
)
|
||||
|
||||
return Path.normalize_nonexisting(filepath, cwd)
|
||||
end
|
||||
|
||||
-- do our best at expanding a path that may or may not exist (ie. check if it
|
||||
-- exists, if so do regular expand, and guess expanded path otherwise)
|
||||
-- Not the clearest name :/
|
||||
function Path.expand_maybe_nonexisting(filepath, cwd)
|
||||
local real_expanded = Path.expand(filepath)
|
||||
if not real_expanded then
|
||||
real_expanded = Path.expand_nonexisting(filepath, cwd)
|
||||
end
|
||||
return real_expanded
|
||||
end
|
||||
|
||||
function Path.normalize_maybe_nonexisting(filepath, cwd)
|
||||
local real_normalized = Path.normalize(filepath)
|
||||
if not real_normalized then
|
||||
real_normalized = Path.normalize_nonexisting(filepath, cwd)
|
||||
end
|
||||
return real_normalized
|
||||
end
|
||||
|
||||
---Return files and directories in path as a list
|
||||
---@param root string
|
||||
---@return string[] files, string[] directories
|
||||
function Path.scandir(root)
|
||||
local files, dirs = {}, {}
|
||||
local fs = uv.fs_scandir(root)
|
||||
if fs then
|
||||
local name, type = "", ""
|
||||
while name do
|
||||
name, type = uv.fs_scandir_next(fs)
|
||||
local path = Path.join(root, name)
|
||||
-- On networked filesystems, it can happen that we get
|
||||
-- a name, but no type. In this case, we must query the
|
||||
-- type manually via fs_stat(). See issue:
|
||||
-- https://github.com/luvit/luv/issues/660
|
||||
if name and not type then
|
||||
local stat = uv.fs_stat(path)
|
||||
type = stat and stat.type
|
||||
end
|
||||
if type == "file" then
|
||||
table.insert(files, path)
|
||||
elseif type == "directory" then
|
||||
table.insert(dirs, path)
|
||||
elseif type == "link" then
|
||||
local followed_path = uv.fs_realpath(path)
|
||||
if followed_path then
|
||||
local stat = uv.fs_stat(followed_path)
|
||||
if stat.type == "file" then
|
||||
table.insert(files, path)
|
||||
elseif stat.type == "directory" then
|
||||
table.insert(dirs, path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return files, dirs
|
||||
end
|
||||
|
||||
---Get basename
|
||||
---@param filepath string
|
||||
---@param ext boolean if true, separate the file extension
|
||||
---@return string, string?
|
||||
---Example:
|
||||
--- Path.basename("~/.config/nvim/init.lua") -> init.lua
|
||||
--- Path.basename("~/.config/nvim/init.lua", true) -> init, lua
|
||||
function Path.basename(filepath, ext)
|
||||
local base = filepath
|
||||
if base:find(sep) then
|
||||
base = base:match(("%s([^%s]+)$"):format(sep, sep))
|
||||
end
|
||||
if ext then
|
||||
return base:match("(.*)%.(.+)")
|
||||
else
|
||||
return base
|
||||
end
|
||||
end
|
||||
|
||||
function Path.extension(fname)
|
||||
return fname:match("%.([^%.]+)$")
|
||||
end
|
||||
|
||||
function Path.components(path)
|
||||
return vim.split(path, sep, { plain = true, trimempty = true })
|
||||
end
|
||||
|
||||
---Get parent of a path, without trailing separator
|
||||
---if path is a directory or does not have a parent, returns nil
|
||||
---Example:
|
||||
--- On platforms that use "\\" backslash as path separator, e.g., Windows:
|
||||
--- Path.parent("C:/project_root/file.txt") -- returns "C:/project_root"
|
||||
--- Path.parent([[C:\project_root\file.txt]]) -- returns [[C:\project_root]]
|
||||
---
|
||||
--- -- the followings return `nil`s
|
||||
--- Path.parent("C:/")
|
||||
--- Path.parent([[C:\]])
|
||||
--- Path.parent([[C:\project_root\]])
|
||||
---
|
||||
--- -- WARN: although it's unlikely that we will reach the driver's root
|
||||
--- -- level, Path.parent("C:\file.txt") returns "C:", and please be
|
||||
--- -- cautious when passing the parent path to some vim functions because
|
||||
--- -- some vim functions on Windows treat "C:" as a file instead:
|
||||
--- -- vim.fn.fnamemodify("C:", ":p") -- returns $CWD .. sep .. "C:"
|
||||
--- -- To get the desired result, use vim.fn.fnamemodify("C:" .. sep, ":p")
|
||||
---
|
||||
--- On platforms that use "/" forward slash as path separator, e.g., linux:
|
||||
--- Path.parent("/project_root/file.txt") returns "/project_root"
|
||||
--- Path.parent("/file.txt") returns ""
|
||||
---
|
||||
--- -- the followings return `nil`s
|
||||
--- Path.parent("/")
|
||||
--- Path.parent("/project_root/")
|
||||
---
|
||||
--- -- backslash in a valid filename character in linux:
|
||||
--- Path.parent([[/project_root/\valid\file\name.txt]]) returns "/project_root"
|
||||
Path.parent = (function()
|
||||
---@alias PathSeparator "/" | "\\"
|
||||
---@param os_sep PathSeparator
|
||||
---@return fun(string): string | nil
|
||||
local function generate_parent(os_sep)
|
||||
if os_sep == "/" then
|
||||
---@param path string
|
||||
---@return string | nil
|
||||
return function(path)
|
||||
local last_component = path:match("[/]+[^/]+$")
|
||||
if not last_component then
|
||||
return nil
|
||||
end
|
||||
|
||||
return path:sub(1, #path - #last_component)
|
||||
end
|
||||
else
|
||||
---@param path string
|
||||
---@return string | nil
|
||||
return function(path)
|
||||
local last_component = path:match("[/\\]+[^/\\]+$")
|
||||
if not last_component then
|
||||
return nil
|
||||
end
|
||||
|
||||
return path:sub(1, #path - #last_component)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- for test only
|
||||
if __LUASNIP_TEST_SEP_OVERRIDE then
|
||||
return generate_parent
|
||||
else
|
||||
return generate_parent(sep)
|
||||
end
|
||||
end)()
|
||||
|
||||
-- returns nil if the file does not exist!
|
||||
Path.normalize = uv.fs_realpath
|
||||
|
||||
return Path
|
||||
@ -0,0 +1,137 @@
|
||||
local is_class = {
|
||||
a = true,
|
||||
c = true,
|
||||
d = true,
|
||||
l = true,
|
||||
p = true,
|
||||
s = true,
|
||||
u = true,
|
||||
w = true,
|
||||
x = true,
|
||||
z = true,
|
||||
-- and uppercase versions.
|
||||
A = true,
|
||||
C = true,
|
||||
D = true,
|
||||
L = true,
|
||||
P = true,
|
||||
S = true,
|
||||
U = true,
|
||||
W = true,
|
||||
X = true,
|
||||
Z = true,
|
||||
-- all others false.
|
||||
}
|
||||
|
||||
local is_rep_mod = {
|
||||
["+"] = true,
|
||||
["*"] = true,
|
||||
["-"] = true,
|
||||
["?"] = true,
|
||||
}
|
||||
|
||||
local function is_escaped(text, indx)
|
||||
local count = 0
|
||||
for i = indx - 1, 1, -1 do
|
||||
if string.sub(text, i, i) == "%" then
|
||||
count = count + 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
return count % 2 == 1
|
||||
end
|
||||
|
||||
local function charset_end_indx(string, start_indx)
|
||||
-- set plain
|
||||
local indx = string:find("]", start_indx, true)
|
||||
-- find unescaped ']'
|
||||
while indx and is_escaped(string, indx) do
|
||||
indx = string:find("]", indx + 1, true)
|
||||
end
|
||||
return indx
|
||||
end
|
||||
|
||||
return {
|
||||
tokenize = function(pattern)
|
||||
local indx = 1
|
||||
local current_text = ""
|
||||
local tokens = {}
|
||||
-- assume the pattern starts with text (as opposed to eg. a character
|
||||
-- class), worst-case an empty textNode is (unnecessarily) inserted at
|
||||
-- the beginning.
|
||||
local is_text = true
|
||||
while indx <= #pattern do
|
||||
local next_indx
|
||||
local next_text
|
||||
local next_is_text
|
||||
-- for some atoms *,+,-,? are not applicable, ignore them.
|
||||
local repeatable = true
|
||||
local char = pattern:sub(indx, indx)
|
||||
if char == "%" then
|
||||
if pattern:sub(indx + 1, indx + 1) == "b" then
|
||||
-- %b seems to consume exactly the next two chars literally.
|
||||
next_is_text = false
|
||||
next_indx = indx + 4
|
||||
repeatable = false
|
||||
elseif is_class[pattern:sub(indx + 1, indx + 1)] then
|
||||
next_is_text = false
|
||||
next_indx = indx + 2
|
||||
else
|
||||
-- not a class, just an escaped character.
|
||||
next_is_text = true
|
||||
next_indx = indx + 2
|
||||
-- only append escaped char, not '%'.
|
||||
end
|
||||
elseif char == "." then
|
||||
next_is_text = false
|
||||
next_indx = indx + 1
|
||||
elseif char == "[" then
|
||||
next_is_text = false
|
||||
-- if not found, just exit loop now, pattern is malformed.
|
||||
next_indx = (charset_end_indx(pattern, indx) or #pattern) + 1
|
||||
elseif
|
||||
char == "("
|
||||
or char == ")"
|
||||
or (char == "^" and indx == 1)
|
||||
then
|
||||
-- ^ is interpreted literally if not at beginning.
|
||||
-- $ will always be interpreted literally in triggers.
|
||||
|
||||
-- remove ( and ) from text.
|
||||
-- keep text or no-text active.
|
||||
next_is_text = is_text
|
||||
-- increase indx to exclude ( from tokens.
|
||||
indx = indx + 1
|
||||
next_indx = indx
|
||||
-- cannot repeat group.
|
||||
repeatable = false
|
||||
else
|
||||
next_is_text = true
|
||||
next_indx = indx + 1
|
||||
end
|
||||
|
||||
if repeatable and is_rep_mod[pattern:sub(next_indx, next_indx)] then
|
||||
next_indx = next_indx + 1
|
||||
next_is_text = false
|
||||
end
|
||||
|
||||
next_text = pattern:sub(indx, next_indx - 1)
|
||||
|
||||
-- check if this token is still the same as the previous.
|
||||
if next_is_text == is_text then
|
||||
current_text = current_text .. next_text
|
||||
else
|
||||
tokens[#tokens + 1] = current_text
|
||||
current_text = next_text
|
||||
end
|
||||
|
||||
indx = next_indx
|
||||
is_text = next_is_text
|
||||
end
|
||||
|
||||
-- add last part, would normally be added at the end of the loop.
|
||||
tokens[#tokens + 1] = current_text
|
||||
return tokens
|
||||
end,
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
local M = {}
|
||||
|
||||
local SELECT_RAW = "LUASNIP_SELECT_RAW"
|
||||
local SELECT_DEDENT = "LUASNIP_SELECT_DEDENT"
|
||||
local TM_SELECT = "LUASNIP_TM_SELECT"
|
||||
|
||||
function M.retrieve()
|
||||
local ok, val = pcall(vim.api.nvim_buf_get_var, 0, SELECT_RAW)
|
||||
if ok then
|
||||
local result = {
|
||||
val,
|
||||
vim.api.nvim_buf_get_var(0, SELECT_DEDENT),
|
||||
vim.api.nvim_buf_get_var(0, TM_SELECT),
|
||||
}
|
||||
|
||||
vim.api.nvim_buf_del_var(0, SELECT_RAW)
|
||||
vim.api.nvim_buf_del_var(0, SELECT_DEDENT)
|
||||
vim.api.nvim_buf_del_var(0, TM_SELECT)
|
||||
|
||||
return unpack(result)
|
||||
end
|
||||
return {}, {}, {}
|
||||
end
|
||||
|
||||
local function get_min_indent(lines)
|
||||
-- "^(%s*)%S": match only lines that actually contain text.
|
||||
local min_indent = lines[1]:match("^(%s*)%S")
|
||||
for i = 2, #lines do
|
||||
-- %s* -> at least matches
|
||||
local line_indent = lines[i]:match("^(%s*)%S")
|
||||
-- ignore if not matched.
|
||||
if line_indent then
|
||||
-- if no line until now matched, use line_indent.
|
||||
if not min_indent or #line_indent < #min_indent then
|
||||
min_indent = line_indent
|
||||
end
|
||||
end
|
||||
end
|
||||
return min_indent
|
||||
end
|
||||
|
||||
local function store_registers(...)
|
||||
local names = { ... }
|
||||
local restore_data = {}
|
||||
for _, name in ipairs(names) do
|
||||
restore_data[name] = {
|
||||
data = vim.fn.getreg(name),
|
||||
type = vim.fn.getregtype(name),
|
||||
}
|
||||
end
|
||||
return restore_data
|
||||
end
|
||||
|
||||
local function restore_registers(restore_data)
|
||||
for name, name_restore_data in pairs(restore_data) do
|
||||
vim.fn.setreg(name, name_restore_data.data, name_restore_data.type)
|
||||
end
|
||||
end
|
||||
|
||||
-- subtle: `:lua` exits VISUAL, which means that the '< '>-marks will be set correctly!
|
||||
-- Afterwards, we can just use <cmd>lua, which does not change the mode.
|
||||
M.select_keys =
|
||||
[[:lua require("luasnip.util.select").pre_cut()<Cr>gv"zs<cmd>lua require('luasnip.util.select').post_cut("z")<Cr>]]
|
||||
|
||||
local saved_registers
|
||||
local lines
|
||||
local start_line, start_col, end_line, end_col
|
||||
local mode
|
||||
function M.pre_cut()
|
||||
-- store registers so we don't change any of them.
|
||||
-- "" is affected since we perform a cut (s), 1-9 also (although :h
|
||||
-- quote_number seems to state otherwise for cuts to specific registers..?).
|
||||
saved_registers =
|
||||
store_registers("", "1", "2", "3", "4", "5", "6", "7", "8", "9", "z")
|
||||
|
||||
-- store data needed for de-indenting lines.
|
||||
start_line = vim.fn.line("'<") - 1
|
||||
start_col = vim.fn.col("'<")
|
||||
end_line = vim.fn.line("'>") - 1
|
||||
end_col = vim.fn.col("'>")
|
||||
-- +1: include final line.
|
||||
lines = vim.api.nvim_buf_get_lines(0, start_line, end_line + 1, true)
|
||||
mode = vim.fn.visualmode()
|
||||
end
|
||||
|
||||
function M.post_cut(register_name)
|
||||
-- remove trailing newline.
|
||||
local chunks = vim.split(vim.fn.getreg(register_name):gsub("\n$", ""), "\n")
|
||||
|
||||
-- make sure to restore the registers to the state they were before cutting.
|
||||
restore_registers(saved_registers)
|
||||
|
||||
local tm_select, select_dedent = vim.deepcopy(chunks), vim.deepcopy(chunks)
|
||||
|
||||
local min_indent = get_min_indent(lines) or ""
|
||||
if mode == "V" then
|
||||
tm_select[1] = tm_select[1]:gsub("^%s+", "")
|
||||
-- remove indent from all lines:
|
||||
for i = 1, #select_dedent do
|
||||
select_dedent[i] = select_dedent[i]:gsub("^" .. min_indent, "")
|
||||
end
|
||||
-- due to the trailing newline of the last line, and vim.split's
|
||||
-- behaviour, the last line of `chunks` is always empty.
|
||||
-- Keep this
|
||||
elseif mode == "v" then
|
||||
-- if selection starts inside indent, remove indent.
|
||||
if #min_indent > start_col then
|
||||
select_dedent[1] = lines[1]:gsub(min_indent, "")
|
||||
end
|
||||
for i = 2, #select_dedent - 1 do
|
||||
select_dedent[i] = select_dedent[i]:gsub(min_indent, "")
|
||||
end
|
||||
|
||||
-- remove as much indent from the last line as possible.
|
||||
if #min_indent > end_col then
|
||||
select_dedent[#select_dedent] = ""
|
||||
else
|
||||
select_dedent[#select_dedent] =
|
||||
select_dedent[#select_dedent]:gsub("^" .. min_indent, "")
|
||||
end
|
||||
else
|
||||
-- in block: if indent is in block, remove the part of it that is inside
|
||||
-- it for select_dedent.
|
||||
if #min_indent > start_col then
|
||||
local indent_in_block = min_indent:sub(start_col, #min_indent)
|
||||
for i, line in ipairs(chunks) do
|
||||
select_dedent[i] = line:gsub("^" .. indent_in_block, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_var(0, SELECT_RAW, chunks)
|
||||
vim.api.nvim_buf_set_var(0, SELECT_DEDENT, select_dedent)
|
||||
vim.api.nvim_buf_set_var(0, TM_SELECT, tm_select)
|
||||
|
||||
lines = nil
|
||||
start_line, start_col, end_line, end_col = nil, nil, nil, nil
|
||||
mode = nil
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,141 @@
|
||||
-- Some string processing utility functions
|
||||
local M = {}
|
||||
|
||||
---In-place dedents strings in lines.
|
||||
---@param lines string[].
|
||||
local function dedent(lines)
|
||||
if #lines > 0 then
|
||||
local ind_size = math.huge
|
||||
for i, _ in ipairs(lines) do
|
||||
local i1, i2 = lines[i]:find("^%s*[^%s]")
|
||||
if i1 and i2 < ind_size then
|
||||
ind_size = i2
|
||||
end
|
||||
end
|
||||
for i, _ in ipairs(lines) do
|
||||
lines[i] = lines[i]:sub(ind_size, -1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Applies opts to lines.
|
||||
---lines is modified in-place.
|
||||
---@param lines string[].
|
||||
---@param options table, required, can have values:
|
||||
--- - trim_empty: removes empty first and last lines.
|
||||
--- - dedent: removes indent common to all lines.
|
||||
function M.process_multiline(lines, options)
|
||||
if options.trim_empty then
|
||||
if lines[1]:match("^%s*$") then
|
||||
table.remove(lines, 1)
|
||||
end
|
||||
if #lines > 0 and lines[#lines]:match("^%s*$") then
|
||||
lines[#lines] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if options.dedent then
|
||||
dedent(lines)
|
||||
end
|
||||
end
|
||||
|
||||
function M.dedent(s)
|
||||
local lst = vim.split(s, "\n")
|
||||
dedent(lst)
|
||||
return table.concat(lst, "\n")
|
||||
end
|
||||
|
||||
local function is_escaped(s, indx)
|
||||
local count = 0
|
||||
for i = indx - 1, 1, -1 do
|
||||
if string.sub(s, i, i) == "\\" then
|
||||
count = count + 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
return count % 2 == 1
|
||||
end
|
||||
|
||||
--- return position of next (relative to `start`) unescaped occurence of
|
||||
--- `target` in `s`.
|
||||
---@param s string
|
||||
---@param target string
|
||||
---@param start number
|
||||
local function find_next_unescaped(s, target, start)
|
||||
while true do
|
||||
local from = s:find(target, start, true)
|
||||
if not from then
|
||||
return nil
|
||||
end
|
||||
if not is_escaped(s, from) then
|
||||
return from
|
||||
end
|
||||
start = from + 1
|
||||
end
|
||||
end
|
||||
|
||||
--- Creates iterator that returns all positions of substrings <left>.*<right>
|
||||
--- in `s`, where left and right are not escaped.
|
||||
--- Only complete pairs left,right are returned, an unclosed left is ignored.
|
||||
---@param s string
|
||||
---@param left string
|
||||
---@param right string
|
||||
---@return function: iterator, returns pairs from,to.
|
||||
function M.unescaped_pairs(s, left, right)
|
||||
local search_from = 1
|
||||
|
||||
return function()
|
||||
local match_from = find_next_unescaped(s, left, search_from)
|
||||
if not match_from then
|
||||
return nil
|
||||
end
|
||||
local match_to = find_next_unescaped(s, right, match_from + 1)
|
||||
if not match_to then
|
||||
return nil
|
||||
end
|
||||
|
||||
search_from = match_to + 1
|
||||
return match_from, match_to
|
||||
end
|
||||
end
|
||||
|
||||
function M.aupatescape(s)
|
||||
if vim.fn.has("win32") or vim.fn.has("win64") then
|
||||
-- windows: replace \ with / for au-pattern.
|
||||
s, _ = s:gsub("\\", "/")
|
||||
end
|
||||
local escaped, _ = s:gsub(",", "\\,")
|
||||
return vim.fn.fnameescape(escaped)
|
||||
end
|
||||
|
||||
function M.sanitize(str)
|
||||
return str:gsub("%\r", "")
|
||||
end
|
||||
|
||||
-- string-operations implemented according to
|
||||
-- https://github.com/microsoft/vscode/blob/71c221c532996c9976405f62bb888283c0cf6545/src/vs/editor/contrib/snippet/browser/snippetParser.ts#L372-L415
|
||||
-- such that they can be used for snippet-transformations in vscode-snippets.
|
||||
local function capitalize(str)
|
||||
-- uppercase first character.
|
||||
return str:gsub("^.", string.upper)
|
||||
end
|
||||
local function pascalcase(str)
|
||||
local pascalcased = ""
|
||||
for match in str:gmatch("[a-zA-Z0-9]+") do
|
||||
pascalcased = pascalcased .. capitalize(match)
|
||||
end
|
||||
return pascalcased
|
||||
end
|
||||
M.vscode_string_modifiers = {
|
||||
upcase = string.upper,
|
||||
downcase = string.lower,
|
||||
capitalize = capitalize,
|
||||
pascalcase = pascalcase,
|
||||
camelcase = function(str)
|
||||
-- same as pascalcase, but first character lowercased.
|
||||
return pascalcase(str):gsub("^.", string.lower)
|
||||
end,
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,39 @@
|
||||
---Convert set of values to a list of those values.
|
||||
---@generic T
|
||||
---@param tbl T|T[]|table<T, boolean>
|
||||
---@return table<T, boolean>
|
||||
local function set_to_list(tbl)
|
||||
local ls = {}
|
||||
|
||||
for v, _ in pairs(tbl) do
|
||||
table.insert(ls, v)
|
||||
end
|
||||
|
||||
return ls
|
||||
end
|
||||
|
||||
---Convert value or list of values to a table of booleans for fast lookup.
|
||||
---@generic T
|
||||
---@param values T|T[]|table<T, boolean>
|
||||
---@return table<T, boolean>
|
||||
local function list_to_set(values)
|
||||
if values == nil then
|
||||
return {}
|
||||
end
|
||||
|
||||
if type(values) ~= "table" then
|
||||
return { [values] = true }
|
||||
end
|
||||
|
||||
local list = {}
|
||||
for _, v in ipairs(values) do
|
||||
list[v] = true
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
return {
|
||||
list_to_set = list_to_set,
|
||||
set_to_list = set_to_list,
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
-- http://lua-users.org/wiki/TimeZone
|
||||
local function get_timezone_offset(ts)
|
||||
local utcdate = os.date("!*t", ts)
|
||||
local localdate = os.date("*t", ts)
|
||||
localdate.isdst = false -- this is the trick
|
||||
local diff = os.difftime(os.time(localdate), os.time(utcdate))
|
||||
local h, m = math.modf(diff / 3600)
|
||||
return string.format("%+.4d", 100 * h + 60 * m)
|
||||
end
|
||||
|
||||
return {
|
||||
get_timezone_offset = get_timezone_offset,
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
return {
|
||||
textNode = 1,
|
||||
insertNode = 2,
|
||||
functionNode = 3,
|
||||
snippetNode = 4,
|
||||
choiceNode = 5,
|
||||
dynamicNode = 6,
|
||||
snippet = 7,
|
||||
exitNode = 8,
|
||||
restoreNode = 9,
|
||||
node_types = { 1, 2, 3, 4, 5, 6, 7, 8, 9 },
|
||||
names = {
|
||||
"textNode",
|
||||
"insertNode",
|
||||
"functionNode",
|
||||
"snippetNode",
|
||||
"choiceNode",
|
||||
"dynamicNode",
|
||||
"snippet",
|
||||
"exitNode",
|
||||
"restoreNode",
|
||||
},
|
||||
names_pascal_case = {
|
||||
"TextNode",
|
||||
"InsertNode",
|
||||
"FunctionNode",
|
||||
"SnippetNode",
|
||||
"ChoiceNode",
|
||||
"DynamicNode",
|
||||
"Snippet",
|
||||
"ExitNode",
|
||||
"RestoreNode",
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,522 @@
|
||||
local session = require("luasnip.session")
|
||||
|
||||
local function get_cursor_0ind()
|
||||
local c = vim.api.nvim_win_get_cursor(0)
|
||||
c[1] = c[1] - 1
|
||||
return c
|
||||
end
|
||||
|
||||
-- don't use utf-indexed column, win_set_cursor ignores these.
|
||||
local function set_cursor_0ind(c)
|
||||
c[1] = c[1] + 1
|
||||
vim.api.nvim_win_set_cursor(0, c)
|
||||
end
|
||||
|
||||
-- pos: (0,0)-indexed.
|
||||
local function line_chars_before(pos)
|
||||
-- cur-rows are 1-indexed, api-rows 0.
|
||||
local line = vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)
|
||||
return string.sub(line[1], 1, pos[2])
|
||||
end
|
||||
|
||||
-- returns current line with text up-to and excluding the cursor.
|
||||
local function get_current_line_to_cursor()
|
||||
return line_chars_before(get_cursor_0ind())
|
||||
end
|
||||
|
||||
-- delete n chars before cursor, MOVES CURSOR
|
||||
local function remove_n_before_cur(n)
|
||||
local cur = get_cursor_0ind()
|
||||
vim.api.nvim_buf_set_text(0, cur[1], cur[2] - n, cur[1], cur[2], { "" })
|
||||
cur[2] = cur[2] - n
|
||||
set_cursor_0ind(cur)
|
||||
end
|
||||
|
||||
-- in-place modifies the table.
|
||||
local function dedent(text, indentstring)
|
||||
-- 2 because 1 shouldn't contain indent.
|
||||
for i = 2, #text do
|
||||
text[i] = text[i]:gsub("^" .. indentstring, "")
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
-- in-place insert indenstrig before each line.
|
||||
local function indent(text, indentstring)
|
||||
for i = 2, #text - 1, 1 do
|
||||
-- only indent if there is actually text.
|
||||
if #text[i] > 0 then
|
||||
text[i] = indentstring .. text[i]
|
||||
end
|
||||
end
|
||||
-- assuming that the last line should be indented as it is probably
|
||||
-- followed by some other node, therefore isn't an empty line.
|
||||
if #text > 1 then
|
||||
text[#text] = indentstring .. text[#text]
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
--- In-place expands tabs in `text`.
|
||||
--- Difficulties:
|
||||
--- we cannot simply replace tabs with a given number of spaces, the tabs align
|
||||
--- text at multiples of `tabwidth`. This is also the reason we need the number
|
||||
--- of columns the text is already indented by (otherwise we can only start a 0).
|
||||
---@param text string[], multiline string.
|
||||
---@param tabwidth number, displaycolumns one tab should shift following text
|
||||
--- by.
|
||||
---@param parent_indent_displaycolumns number, displaycolumn this text is
|
||||
--- already at.
|
||||
---@return string[], `text` (only for simple nesting).
|
||||
local function expand_tabs(text, tabwidth, parent_indent_displaycolumns)
|
||||
for i, line in ipairs(text) do
|
||||
local new_line = ""
|
||||
local start_indx = 1
|
||||
while true do
|
||||
local tab_indx = line:find("\t", start_indx, true)
|
||||
-- if no tab found, sub till end (ie. -1).
|
||||
new_line = new_line .. line:sub(start_indx, (tab_indx or 0) - 1)
|
||||
if tab_indx then
|
||||
-- #new_line is index of this tab in new_line.
|
||||
new_line = new_line
|
||||
.. string.rep(
|
||||
" ",
|
||||
tabwidth
|
||||
- (
|
||||
(parent_indent_displaycolumns + #new_line)
|
||||
% tabwidth
|
||||
)
|
||||
)
|
||||
else
|
||||
-- reached end of string.
|
||||
break
|
||||
end
|
||||
start_indx = tab_indx + 1
|
||||
end
|
||||
text[i] = new_line
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
local function tab_width()
|
||||
return vim.bo.shiftwidth ~= 0 and vim.bo.shiftwidth or vim.bo.tabstop
|
||||
end
|
||||
|
||||
local function mark_pos_equal(m1, m2)
|
||||
local p1 = vim.api.nvim_buf_get_extmark_by_id(0, session.ns_id, m1, {})
|
||||
local p2 = vim.api.nvim_buf_get_extmark_by_id(0, session.ns_id, m2, {})
|
||||
return p1[1] == p2[1] and p1[2] == p2[2]
|
||||
end
|
||||
|
||||
local function move_to_mark(id)
|
||||
local new_cur_pos
|
||||
new_cur_pos = vim.api.nvim_buf_get_extmark_by_id(
|
||||
0,
|
||||
session.ns_id,
|
||||
id,
|
||||
{ details = false }
|
||||
)
|
||||
set_cursor_0ind(new_cur_pos)
|
||||
end
|
||||
|
||||
local function bytecol_to_utfcol(pos)
|
||||
local line = vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)
|
||||
-- line[1]: get_lines returns table.
|
||||
return { pos[1], vim.str_utfindex(line[1] or "", pos[2]) }
|
||||
end
|
||||
|
||||
local function replace_feedkeys(keys, opts)
|
||||
vim.api.nvim_feedkeys(
|
||||
vim.api.nvim_replace_termcodes(keys, true, false, true),
|
||||
-- folds are opened manually now, no need to pass t.
|
||||
-- n prevents langmap from interfering.
|
||||
opts or "n",
|
||||
true
|
||||
)
|
||||
end
|
||||
|
||||
-- pos: (0,0)-indexed.
|
||||
local function cursor_set_keys(pos, before)
|
||||
if before then
|
||||
if pos[2] == 0 then
|
||||
pos[1] = pos[1] - 1
|
||||
-- pos2 is set to last columnt of previous line.
|
||||
-- # counts bytes, but win_set_cursor expects bytes, so all's good.
|
||||
pos[2] =
|
||||
#vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1]
|
||||
else
|
||||
pos[2] = pos[2] - 1
|
||||
end
|
||||
end
|
||||
|
||||
return "<cmd>lua vim.api.nvim_win_set_cursor(0,{"
|
||||
-- +1, win_set_cursor starts at 1.
|
||||
.. pos[1] + 1
|
||||
.. ","
|
||||
-- -1 works for multibyte because of rounding, apparently.
|
||||
.. pos[2]
|
||||
.. "})"
|
||||
.. "<cr><cmd>:silent! foldopen!<cr>"
|
||||
end
|
||||
|
||||
-- any for any mode.
|
||||
-- other functions prefixed with eg. normal have to be in that mode, the
|
||||
-- initial esc removes that need.
|
||||
local function any_select(b, e)
|
||||
-- stylua: ignore
|
||||
replace_feedkeys(
|
||||
-- this esc -> movement sometimes leads to a slight flicker
|
||||
-- TODO: look into preventing that reliably.
|
||||
-- Go into visual, then place endpoints.
|
||||
-- This is to allow us to place the cursor on the \n of a line.
|
||||
-- see #1158
|
||||
"<esc>"
|
||||
-- open folds that contain this selection.
|
||||
-- we assume that the selection is contained in at most one fold, and
|
||||
-- that that fold covers b.
|
||||
-- if we open the fold while visual is active, the selection will be
|
||||
-- wrong, so this is necessary before we enter VISUAL.
|
||||
.. cursor_set_keys(b)
|
||||
-- start visual highlight and move to b again.
|
||||
-- since we are now in visual, this might actually move the cursor.
|
||||
.. "v"
|
||||
.. cursor_set_keys(b)
|
||||
-- swap to other end of selection, and move it to e.
|
||||
.. "o"
|
||||
.. (vim.o.selection == "exclusive" and
|
||||
cursor_set_keys(e) or
|
||||
-- set before
|
||||
cursor_set_keys(e, true))
|
||||
.. "o<C-G><C-r>_" )
|
||||
end
|
||||
|
||||
local function normal_move_on_insert(new_cur_pos)
|
||||
-- moving in normal and going into insert is kind of annoying, eg. when the
|
||||
-- cursor is, in normal, on a tab, i will set it on the beginning of the
|
||||
-- tab. There's more problems, but this is very safe.
|
||||
replace_feedkeys("i" .. cursor_set_keys(new_cur_pos))
|
||||
end
|
||||
|
||||
local function insert_move_on(new_cur_pos)
|
||||
-- maybe feedkeys this too.
|
||||
set_cursor_0ind(new_cur_pos)
|
||||
vim.api.nvim_command("redraw!")
|
||||
end
|
||||
|
||||
local function multiline_equal(t1, t2)
|
||||
for i, line in ipairs(t1) do
|
||||
if line ~= t2[i] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return #t1 == #t2
|
||||
end
|
||||
|
||||
local function word_under_cursor(cur, line)
|
||||
local ind_start = 1
|
||||
local ind_end = #line
|
||||
|
||||
while true do
|
||||
local tmp = string.find(line, "%W%w", ind_start)
|
||||
if not tmp then
|
||||
break
|
||||
end
|
||||
if tmp > cur[2] + 1 then
|
||||
break
|
||||
end
|
||||
ind_start = tmp + 1
|
||||
end
|
||||
|
||||
local tmp = string.find(line, "%w%W", cur[2] + 1)
|
||||
if tmp then
|
||||
ind_end = tmp
|
||||
end
|
||||
|
||||
return string.sub(line, ind_start, ind_end)
|
||||
end
|
||||
|
||||
-- Put text and update cursor(pos) where cursor is byte-indexed.
|
||||
local function put(text, pos)
|
||||
vim.api.nvim_buf_set_text(0, pos[1], pos[2], pos[1], pos[2], text)
|
||||
-- add rows
|
||||
pos[1] = pos[1] + #text - 1
|
||||
-- add columns, start at 0 if no rows were added, else at old col-value.
|
||||
pos[2] = (#text > 1 and 0 or pos[2]) + #text[#text]
|
||||
end
|
||||
|
||||
--[[ Wraps the value in a table if it's not one, makes
|
||||
the first element an empty str if the table is empty]]
|
||||
local function to_string_table(value)
|
||||
if not value then
|
||||
return { "" }
|
||||
end
|
||||
if type(value) == "string" then
|
||||
return { value }
|
||||
end
|
||||
-- at this point it's a table
|
||||
if #value == 0 then
|
||||
return { "" }
|
||||
end
|
||||
-- non empty table
|
||||
return value
|
||||
end
|
||||
|
||||
-- Wrap node in a table if it is not one
|
||||
local function wrap_nodes(nodes)
|
||||
-- safe to assume, if nodes has a metatable, it is a single node, not a
|
||||
-- table.
|
||||
if getmetatable(nodes) and nodes.type then
|
||||
return { nodes }
|
||||
else
|
||||
return nodes
|
||||
end
|
||||
end
|
||||
|
||||
local function pos_equal(p1, p2)
|
||||
return p1[1] == p2[1] and p1[2] == p2[2]
|
||||
end
|
||||
|
||||
local function string_wrap(lines, pos)
|
||||
local new_lines = vim.deepcopy(lines)
|
||||
if #new_lines == 1 and #new_lines[1] == 0 then
|
||||
return { "$" .. (pos and tostring(pos) or "{}") }
|
||||
end
|
||||
new_lines[1] = "${"
|
||||
.. (pos and (tostring(pos) .. ":") or "")
|
||||
.. new_lines[1]
|
||||
new_lines[#new_lines] = new_lines[#new_lines] .. "}"
|
||||
return new_lines
|
||||
end
|
||||
|
||||
-- Heuristic to extract the comment style from the commentstring
|
||||
local _comments_cache = {}
|
||||
local function buffer_comment_chars()
|
||||
local commentstring = vim.bo.commentstring
|
||||
if _comments_cache[commentstring] then
|
||||
return _comments_cache[commentstring]
|
||||
end
|
||||
local comments = { "//", "/*", "*/" }
|
||||
local placeholder = "%s"
|
||||
local index_placeholder = commentstring:find(vim.pesc(placeholder))
|
||||
if index_placeholder then
|
||||
index_placeholder = index_placeholder - 1
|
||||
if index_placeholder + #placeholder == #commentstring then
|
||||
comments[1] = vim.trim(commentstring:sub(1, -#placeholder - 1))
|
||||
else
|
||||
comments[2] = vim.trim(commentstring:sub(1, index_placeholder))
|
||||
comments[3] = vim.trim(
|
||||
commentstring:sub(index_placeholder + #placeholder + 1, -1)
|
||||
)
|
||||
end
|
||||
end
|
||||
_comments_cache[commentstring] = comments
|
||||
return comments
|
||||
end
|
||||
|
||||
local function to_line_table(table_or_string)
|
||||
local tbl = to_string_table(table_or_string)
|
||||
|
||||
-- split entries at \n.
|
||||
local line_table = {}
|
||||
for _, str in ipairs(tbl) do
|
||||
local split = vim.split(str, "\n", true)
|
||||
for i = 1, #split do
|
||||
line_table[#line_table + 1] = split[i]
|
||||
end
|
||||
end
|
||||
|
||||
return line_table
|
||||
end
|
||||
|
||||
local function find_outer_snippet(node)
|
||||
while node.parent do
|
||||
node = node.parent
|
||||
end
|
||||
return node
|
||||
end
|
||||
|
||||
local function redirect_filetypes(fts)
|
||||
local snippet_fts = {}
|
||||
|
||||
for _, ft in ipairs(fts) do
|
||||
vim.list_extend(snippet_fts, session.ft_redirect[ft])
|
||||
end
|
||||
|
||||
return snippet_fts
|
||||
end
|
||||
|
||||
local function deduplicate(list)
|
||||
vim.validate({ list = { list, "table" } })
|
||||
local ret = {}
|
||||
local contains = {}
|
||||
for _, v in ipairs(list) do
|
||||
if not contains[v] then
|
||||
table.insert(ret, v)
|
||||
contains[v] = true
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function get_snippet_filetypes()
|
||||
local config = require("luasnip.session").config
|
||||
local fts = config.ft_func()
|
||||
-- add all last.
|
||||
table.insert(fts, "all")
|
||||
|
||||
return deduplicate(redirect_filetypes(fts))
|
||||
end
|
||||
|
||||
local function pos_add(p1, p2)
|
||||
return { p1[1] + p2[1], p1[2] + p2[2] }
|
||||
end
|
||||
local function pos_sub(p1, p2)
|
||||
return { p1[1] - p2[1], p1[2] - p2[2] }
|
||||
end
|
||||
|
||||
local function pop_front(list)
|
||||
local front = list[1]
|
||||
for i = 2, #list do
|
||||
list[i - 1] = list[i]
|
||||
end
|
||||
list[#list] = nil
|
||||
return front
|
||||
end
|
||||
|
||||
local function sorted_keys(t)
|
||||
local s = {}
|
||||
local i = 1
|
||||
for k, _ in pairs(t) do
|
||||
s[i] = k
|
||||
i = i + 1
|
||||
end
|
||||
table.sort(s)
|
||||
return s
|
||||
end
|
||||
|
||||
-- from https://www.lua.org/pil/19.3.html
|
||||
local function key_sorted_pairs(t)
|
||||
local sorted = sorted_keys(t)
|
||||
local i = 0
|
||||
return function()
|
||||
i = i + 1
|
||||
if sorted[i] == nil then
|
||||
return nil
|
||||
else
|
||||
return sorted[i], t[sorted[i]], i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function no_region_check_wrap(fn, ...)
|
||||
session.jump_active = true
|
||||
-- will run on next tick, after autocommands (especially CursorMoved) for this are done.
|
||||
vim.schedule(function()
|
||||
session.jump_active = false
|
||||
end)
|
||||
return fn(...)
|
||||
end
|
||||
|
||||
local function id(a)
|
||||
return a
|
||||
end
|
||||
|
||||
local function no()
|
||||
return false
|
||||
end
|
||||
|
||||
local function yes()
|
||||
return true
|
||||
end
|
||||
|
||||
local function reverse_lookup(t)
|
||||
local rev = {}
|
||||
for k, v in pairs(t) do
|
||||
rev[v] = k
|
||||
end
|
||||
return rev
|
||||
end
|
||||
|
||||
local function nop() end
|
||||
|
||||
local function indx_of(t, v)
|
||||
for i, value in ipairs(t) do
|
||||
if v == value then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ternary(cond, if_val, else_val)
|
||||
if cond == true then
|
||||
return if_val
|
||||
else
|
||||
return else_val
|
||||
end
|
||||
end
|
||||
|
||||
-- just compare two integers.
|
||||
local function cmp(i1, i2)
|
||||
-- lets hope this ends up as one cmp.
|
||||
if i1 < i2 then
|
||||
return -1
|
||||
end
|
||||
if i1 > i2 then
|
||||
return 1
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- compare two positions, <0 => pos1<pos2, 0 => pos1=pos2, >0 => pos1 > pos2.
|
||||
local function pos_cmp(pos1, pos2)
|
||||
-- if row is different it determines result, otherwise the column does.
|
||||
return 2 * cmp(pos1[1], pos2[1]) + cmp(pos1[2], pos2[2])
|
||||
end
|
||||
|
||||
return {
|
||||
get_cursor_0ind = get_cursor_0ind,
|
||||
set_cursor_0ind = set_cursor_0ind,
|
||||
move_to_mark = move_to_mark,
|
||||
normal_move_on_insert = normal_move_on_insert,
|
||||
insert_move_on = insert_move_on,
|
||||
any_select = any_select,
|
||||
remove_n_before_cur = remove_n_before_cur,
|
||||
get_current_line_to_cursor = get_current_line_to_cursor,
|
||||
line_chars_before = line_chars_before,
|
||||
mark_pos_equal = mark_pos_equal,
|
||||
multiline_equal = multiline_equal,
|
||||
word_under_cursor = word_under_cursor,
|
||||
put = put,
|
||||
to_string_table = to_string_table,
|
||||
wrap_nodes = wrap_nodes,
|
||||
pos_equal = pos_equal,
|
||||
dedent = dedent,
|
||||
indent = indent,
|
||||
expand_tabs = expand_tabs,
|
||||
tab_width = tab_width,
|
||||
buffer_comment_chars = buffer_comment_chars,
|
||||
string_wrap = string_wrap,
|
||||
to_line_table = to_line_table,
|
||||
find_outer_snippet = find_outer_snippet,
|
||||
redirect_filetypes = redirect_filetypes,
|
||||
get_snippet_filetypes = get_snippet_filetypes,
|
||||
json_decode = vim.json.decode,
|
||||
json_encode = vim.json.encode,
|
||||
bytecol_to_utfcol = bytecol_to_utfcol,
|
||||
pos_sub = pos_sub,
|
||||
pos_add = pos_add,
|
||||
deduplicate = deduplicate,
|
||||
pop_front = pop_front,
|
||||
key_sorted_pairs = key_sorted_pairs,
|
||||
no_region_check_wrap = no_region_check_wrap,
|
||||
id = id,
|
||||
no = no,
|
||||
yes = yes,
|
||||
reverse_lookup = reverse_lookup,
|
||||
nop = nop,
|
||||
indx_of = indx_of,
|
||||
ternary = ternary,
|
||||
pos_cmp = pos_cmp,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@
|
||||
" 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
|
||||
@ -0,0 +1,37 @@
|
||||
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' } ,
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
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",
|
||||
})
|
||||
@ -0,0 +1,23 @@
|
||||
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
|
||||
@ -0,0 +1,116 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
" 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
|
||||
@ -0,0 +1,30 @@
|
||||
name: Format JSON files
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the "main" branch
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# Make sure the actual branch is checked out when running on pull requests
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Format JSON
|
||||
uses: creyD/prettier_action@v4.3
|
||||
with:
|
||||
commit_message: "CI: format JSON files"
|
||||
only_changed: true
|
||||
# This part is also where you can pass other options, for example:
|
||||
prettier_options: --tab-width 4 --parser json --write **/*.json
|
||||
@ -0,0 +1 @@
|
||||
require('cmp').register_source('path', require('cmp_path').new())
|
||||
Reference in New Issue
Block a user