Merge branch 'master' into make_jsregexp_simplify

This commit is contained in:
L3MON4D3 2023-06-16 12:23:30 +02:00 committed by GitHub
commit b72ae3866f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 850 additions and 169 deletions

2
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "deps/jsregexp"]
path = deps/jsregexp
url = https://github.com/kmarius/jsregexp/
url = ../../kmarius/jsregexp/

109
DOC.md
View file

@ -212,6 +212,8 @@ s({trig="trigger"}, {})
LuaSnip on snippet expansion (and thus has access to the matched trigger and
captures), while `show_condition` is (should be) evaluated by the
completion engines when scanning for available snippet candidates.
- `filetype`: `string`, the filetype of the snippet.
This overrides the filetype the snippet is added (via `add_snippet`) as.
- `nodes`: A single node or a list of nodes. The nodes that make up the
snippet.
@ -2150,7 +2152,7 @@ Luasnip is capable of loading snippets from different formats, including both
the well-established VSCode and SnipMate format, as well as plain Lua files for
snippets written in Lua.
All loaders share a similar interface:
All loaders (except the vscode-standalone-loader) share a similar interface:
`require("luasnip.loaders.from_{vscode,snipmate,lua}").{lazy_,}load(opts:table|nil)`
where `opts` can contain the following keys:
@ -2181,7 +2183,7 @@ filetype is changed luasnip actually loads `lazy_load`ed snippets for the
filetypes associated with this buffer. This association can be changed by
customizing `load_ft_func` in `setup`: the option takes a function that, passed
a `bufnr`, returns the filetypes that should be loaded (`fn(bufnr) -> filetypes
(string[])`)).
(string[])`)).
All of the loaders support reloading, so simply editing any file contributing
snippets will reload its snippets (only in the session the file was edited in;
@ -2191,6 +2193,46 @@ For easy editing of these files, LuaSnip provides a `vim.ui.select`-based dialog
([Loaders-edit_snippets](#edit_snippets)) where first the filetype, and then the
file can be selected.
### Snippet-specific filetypes
Some loaders (vscode,lua) support giving snippets generated in some file their
own filetype (vscode via `scope`, lua via the underlying `filetype`-option for
snippets). These snippet-specific filetypes are not considered when determining
which files to `lazy_load` for some filetype, this is exclusively determined by
the `language` associated with a file in vscodes' `package.json`, and the
file/directory-name in lua.
This can be resolved relatively easily in vscode, where the `language`
advertised in `package.json` can just be a superset of the `scope`s in the file.
Another simplistic solution is to set the language to `all` (in lua, it might
make sense to create a directory `luasnippets/all/*.lua` to group these files
together).
Another approach is to modify `load_ft_func` to load a custom filetype if the
snippets should be activated, and store the snippets in a file for that
filetype. This can be used to group snippets by e.g. framework, and load them
once a file belonging to such a framework is edited.
**Example**:
`react.lua`
```lua
return {
s({filetype = "css", trig = ...}, ...),
s({filetype = "html", trig = ...}, ...),
s({filetype = "js", trig = ...}, ...),
}
```
`luasnip_config.lua`
```lua
load_ft_func = function(bufnr)
if "<bufnr-in-react-framework>" then
-- will load `react.lua` for this buffer
return {"react"}
else
return require("luasnip.extras.filetype_functions").from_filetype_load
end
end
```
## Troubleshooting
* LuaSnip uses `all` as the global filetype. As most snippet collections don't
@ -2313,6 +2355,69 @@ require("luasnip.loaders.from_vscode").lazy_load({paths = "~/.config/nvim/my_sni
require("luasnip.loaders.from_vscode").load({paths = "./my_snippets"})
```
### Standalone
Beside snippet-libraries provided by packages, vscode also supports another
format which can be used for project-local snippets, or user-defined snippets,
`.code-snippets`.
The layout of these files is almost identical to that of the package-provided
snippets, but there is one additional field supported in the
snippet-definitions, `scope`, with which the filetype of the snippet can be set.
If `scope` is not set, the snippet will be added to the global filetype (`all`).
`require("luasnip.loaders.from_vscode").load_standalone(opts)`
- `opts`: `table`, can contain the following keys:
- `path`: `string`, Path to the `*.code-snippets`-file that should be loaded.
Just like the paths in `load`, this one can begin with a `"~/"` to be
relative to `$HOME`, and a `"./"` to be relative to the
neovim-config-directory.
- `{override,default}_priority`: These keys are passed straight to the
`add_snippets`-calls (documented in [API](#api)) and can be used to change
the priority of the loaded snippets.
**Example**:
`a.code-snippets`:
```jsonc
{
// a comment, since `.code-snippets` may contain jsonc.
"c/cpp-snippet": {
"prefix": [
"trigger1",
"trigger2"
],
"body": [
"this is $1",
"my snippet $2"
],
"description": "A description of the snippet.",
"scope": "c,cpp"
},
"python-snippet": {
"prefix": "trig",
"body": [
"this is $1",
"a different snippet $2"
],
"description": "Another snippet-description.",
"scope": "python"
},
"global snippet": {
"prefix": "trigg",
"body": [
"this is $1",
"the last snippet $2"
],
"description": "One last snippet-description.",
}
}
```
This file can be loaded by calling
```lua
require("luasnip.loaders.from_vscode").load_standalone({path = "a.code-snippets"})
```
## SNIPMATE
Luasnip does not support the full snipmate format: Only `./{ft}.snippets` and

View file

@ -284,6 +284,8 @@ The most direct way to define snippets is `s`:
LuaSnip on snippet expansion (and thus has access to the matched trigger and
captures), while `show_condition` is (should be) evaluated by the
completion engines when scanning for available snippet candidates.
- `filetype`: `string`, the filetype of the snippet.
This overrides the filetype the snippet is added (via `add_snippet`) as.
- `nodes`: A single node or a list of nodes. The nodes that make up the snippet.
- `opts`: A table with the following valid keys:
- `callbacks`: Contains functions that are called upon entering/leaving a node of
@ -2015,7 +2017,7 @@ Luasnip is capable of loading snippets from different formats, including both
the well-established VSCode and SnipMate format, as well as plain Lua files for
snippets written in Lua.
All loaders share a similar interface:
All loaders (except the vscode-standalone-loader) share a similar interface:
`require("luasnip.loaders.from_{vscode,snipmate,lua}").{lazy_,}load(opts:table|nil)`
where `opts` can contain the following keys:
@ -2057,6 +2059,47 @@ dialog (|luasnip-loaders-edit_snippets|) where first the filetype, and then the
file can be selected.
SNIPPET-SPECIFIC FILETYPES ~
Some loaders (vscode,lua) support giving snippets generated in some file their
own filetype (vscode via `scope`, lua via the underlying `filetype`-option for
snippets). These snippet-specific filetypes are not considered when determining
which files to `lazy_load` for some filetype, this is exclusively determined by
the `language` associated with a file in vscodes `package.json`, and the
file/directory-name in lua. This can be resolved relatively easily in vscode,
where the `language` advertised in `package.json` can just be a superset of the
`scope`s in the file. Another simplistic solution is to set the language to
`all` (in lua, it might make sense to create a directory
`luasnippets/all/*.lua` to group these files together). Another approach is to
modify `load_ft_func` to load a custom filetype if the snippets should be
activated, and store the snippets in a file for that filetype. This can be used
to group snippets by e.g. framework, and load them once a file belonging to
such a framework is edited.
**Example**: `react.lua`
>lua
return {
s({filetype = "css", trig = ...}, ...),
s({filetype = "html", trig = ...}, ...),
s({filetype = "js", trig = ...}, ...),
}
<
`luasnip_config.lua`
>lua
load_ft_func = function(bufnr)
if "<bufnr-in-react-framework>" then
-- will load `react.lua` for this buffer
return {"react"}
else
return require("luasnip.extras.filetype_functions").from_filetype_load
end
end
<
TROUBLESHOOTING *luasnip-loaders-troubleshooting*
- LuaSnip uses `all` as the global filetype. As most snippet collections dont
@ -2185,6 +2228,73 @@ This collection can be loaded with any of
<
STANDALONE ~
Beside snippet-libraries provided by packages, vscode also supports another
format which can be used for project-local snippets, or user-defined snippets,
`.code-snippets`.
The layout of these files is almost identical to that of the package-provided
snippets, but there is one additional field supported in the
snippet-definitions, `scope`, with which the filetype of the snippet can be
set. If `scope` is not set, the snippet will be added to the global filetype
(`all`).
`require("luasnip.loaders.from_vscode").load_standalone(opts)`
- `opts`: `table`, can contain the following keys:
- `path`: `string`, Path to the `*.code-snippets`-file that should be loaded.
Just like the paths in `load`, this one can begin with a `"~/"` to be
relative to `$HOME`, and a `"./"` to be relative to the
neovim-config-directory.
- `{override,default}_priority`: These keys are passed straight to the
`add_snippets`-calls (documented in |luasnip-api|) and can be used to change
the priority of the loaded snippets.
**Example**: `a.code-snippets`:
>jsonc
{
// a comment, since `.code-snippets` may contain jsonc.
"c/cpp-snippet": {
"prefix": [
"trigger1",
"trigger2"
],
"body": [
"this is $1",
"my snippet $2"
],
"description": "A description of the snippet.",
"scope": "c,cpp"
},
"python-snippet": {
"prefix": "trig",
"body": [
"this is $1",
"a different snippet $2"
],
"description": "Another snippet-description.",
"scope": "python"
},
"global snippet": {
"prefix": "trigg",
"body": [
"this is $1",
"the last snippet $2"
],
"description": "One last snippet-description.",
}
}
<
This file can be loaded by calling
>lua
require("luasnip.loaders.from_vscode").load_standalone({path = "a.code-snippets"})
<
SNIPMATE *luasnip-loaders-snipmate*
Luasnip does not support the full snipmate format: Only `./{ft}.snippets` and

View file

@ -1,4 +1,5 @@
local Source = require("luasnip.session.snippet_collection.source")
local util = require("luasnip.util.util")
-- stylua: ignore
local tsquery_parse =
@ -54,8 +55,8 @@ local function range_highlight(line_start, line_end, hl_duration_ms)
end
end
local function json_find_snippet_definition(bufnr, extension, snippet_name)
local parser_ok, parser = pcall(vim.treesitter.get_parser, bufnr, extension)
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
@ -113,7 +114,12 @@ function M.jump_to_snippet(snip, opts)
end
local fcall_range
if vim.api.nvim_buf_get_name(0):match("%.lua$") then
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.
@ -133,11 +139,9 @@ function M.jump_to_snippet(snip, opts)
return
end
-- matches *.json or *.jsonc.
elseif vim.api.nvim_buf_get_name(0):match("%.jsonc?$") then
local extension = vim.api.nvim_buf_get_name(0):match("jsonc?$")
elseif ft == "json" or ft == "jsonc" then
local ok
ok, fcall_range =
pcall(json_find_snippet_definition, 0, extension, snip.name)
ok, fcall_range = pcall(json_find_snippet_definition, 0, ft, snip.name)
if not ok then
print(
"Could not determine range of snippet-definition: "

View file

@ -40,11 +40,20 @@ local function new_cache()
lazy_loaded_ft = { all = true },
-- key is file type, value are normalized!! paths of .snippets files.
-- shall contain all files loaded by any loader.
ft_paths = {},
-- key is _normalized!!!!_ file path, value are loader-specific.
-- Might contain the snippets from the file, or the filetype(s) it
-- contributes to.
--
-- for vscode:
-- stores {
-- snippets, -- the snippets provided by the file
-- filetype_add_opts, -- add_opts for some filetype
-- filetypes -- filetypes for which this file is active (important for
-- reload).
-- }
path_snippets = {},
}, {
__index = Cache,
@ -52,13 +61,15 @@ local function new_cache()
end
local M = {
vscode = new_cache(),
vscode_packages = new_cache(),
vscode_standalone = new_cache(),
snipmate = new_cache(),
lua = new_cache(),
}
function M.cleanup()
M.vscode:clean()
M.vscode_packages:clean()
M.vscode_standalone:clean()
M.snipmate:clean()
M.lua:clean()
end

View file

@ -1,5 +1,6 @@
local ls = require("luasnip")
local cache = require("luasnip.loaders._caches").vscode
local package_cache = require("luasnip.loaders._caches").vscode_packages
local standalone_cache = require("luasnip.loaders._caches").vscode_standalone
local util = require("luasnip.util.util")
local loader_util = require("luasnip.loaders.util")
local Path = require("luasnip.util.path")
@ -7,10 +8,13 @@ local sp = require("luasnip.nodes.snippetProxy")
local log = require("luasnip.util.log").new("vscode-loader")
local session = require("luasnip.session")
local source = require("luasnip.session.snippet_collection.source")
local multisnippet = require("luasnip.nodes.multiSnippet")
local duplicate = require("luasnip.nodes.duplicate")
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)
@ -21,9 +25,9 @@ local function read_json(fname)
end
local fname_extension = Path.extension(fname)
if fname_extension ~= "json" and fname_extension ~= "jsonc" then
if json_decoders[fname_extension] == nil then
log.error(
"`%s` was expected to have file-extension either `json` or `jsonc`, but doesn't.",
"`%s` was expected to have file-extension either `json`, `jsonc` or `code-snippets`, but doesn't.",
fname
)
return nil
@ -39,110 +43,122 @@ local function read_json(fname)
end
end
-- return all snippets in `file`.
local function get_file_snippets(file)
local lang_snips = {}
local auto_lang_snips = {}
-- since most snippets we load don't have a scope-field, we just insert this 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 {}, {}
return {}
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")
-- There are still some snippets that fail while loading
pcall(function()
-- Sometimes it's a list of prefixes instead of a single one
local prefixes = type(parts.prefix) == "table" and parts.prefix
or { parts.prefix }
for _, prefix in ipairs(prefixes) do
local ls_conf = parts.luasnip or {}
local ls_conf = parts.luasnip or {}
local snip = sp({
trig = prefix,
name = name,
dscr = parts.description or name,
wordTrig = ls_conf.wordTrig,
priority = ls_conf.priority,
}, body)
-- we may generate multiple interfaces to the same snippet
-- (different filetype, different triggers)
if session.config.loaders_store_source then
-- only know file, not line or line_end.
snip._source = source.from_location(file)
end
-- context common to all snippets generated here.
local common_context = {
name = name,
dscr = parts.description or name,
wordTrig = ls_conf.wordTrig,
priority = ls_conf.priority,
snippetType = ls_conf.autotrigger and "autosnippet" or "snippet",
}
if ls_conf.autotrigger then
table.insert(auto_lang_snips, snip)
else
table.insert(lang_snips, snip)
end
-- 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)
end
end
return lang_snips, auto_lang_snips
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
local function load_snippet_files(lang, files, add_opts)
for _, file in ipairs(files) do
if Path.exists(file) then
local lang_snips, auto_lang_snips
local cached_path = cache.path_snippets[file]
if cached_path then
lang_snips = vim.deepcopy(cached_path.snippets)
auto_lang_snips = vim.deepcopy(cached_path.autosnippets)
cached_path.fts[lang] = true
else
lang_snips, auto_lang_snips = get_file_snippets(file)
-- store snippets to prevent parsing the same file more than once.
cache.path_snippets[file] = {
snippets = vim.deepcopy(lang_snips),
autosnippets = vim.deepcopy(auto_lang_snips),
add_opts = add_opts,
fts = { [lang] = true },
}
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
ls.add_snippets(
lang,
lang_snips,
vim.tbl_extend("keep", {
type = "snippets",
-- again, include filetype, same reasoning as with augroup.
key = string.format("__%s_snippets_%s", lang, file),
refresh_notify = false,
}, add_opts)
)
ls.add_snippets(
lang,
auto_lang_snips,
vim.tbl_extend("keep", {
type = "autosnippets",
key = string.format("__%s_autosnippets_%s", lang, file),
refresh_notify = false,
}, add_opts)
)
log.info(
"Adding %s snippets and %s autosnippets for filetype `%s` from %s",
#lang_snips,
#auto_lang_snips,
lang,
file
)
else
log.error(
"Trying to read snippets from file %s, but it does not exist.",
lang,
file
)
table.insert(snippets, snip)
end
end
ls.refresh_notify(lang)
return snippets
end
-- `refresh` to optionally delay refresh_notify.
-- (it has to be called by the caller, for filetype!)
-- opts may contain:
-- `refresh_notify`: refresh snippets for filetype immediately, default false.
-- `force_reload`: don't use cache when reloading, default false
local function load_snippet_file(file, filetype, add_opts, opts)
opts = opts or {}
local refresh_notify =
util.ternary(opts.refresh_notify ~= nil, opts.refresh_notify, false)
local force_reload =
util.ternary(opts.force_reload ~= nil, opts.force_reload, false)
if not Path.exists(file) then
log.error(
"Trying to read snippets from file %s, but it does not exist.",
file
)
return
end
local file_snippets
local cache = package_cache.path_snippets[file]
if cache.snippets and not force_reload then
file_snippets = vim.tbl_map(duplicate.duplicate_addable, cache.snippets)
else
file_snippets = get_file_snippets(file)
-- store snippets as-is (eg. don't copy), they will be copied when read
-- from.
package_cache.path_snippets[file].snippets = file_snippets
end
ls.add_snippets(
filetype,
-- only load snippets matching the language set in `package.json`.
file_snippets,
vim.tbl_extend("keep", {
-- include filetype, a file may contribute snippets to multiple
-- filetypes, and we don't want to remove snippets for ft1 when
-- adding those for ft2.
key = string.format("__%s_snippets_%s", filetype, file),
refresh_notify = refresh_notify,
}, add_opts)
)
log.info("Adding %s snippets from %s", #file_snippets, file)
end
--- Find all files+associated filetypes in a package.
@ -150,8 +166,7 @@ end
--- package.json)
---@param filter function that filters filetypes, generate from in/exclude-list
--- via loader_util.ft_filter.
---@return table, string -> string[] (ft -> files).
--- Paths are normalized.
---@return table: string -> string[] (ft -> files)
local function package_files(root, filter)
local package = Path.join(root, "package.json")
-- if root doesn't contain a package.json, or it contributes no snippets,
@ -221,7 +236,9 @@ local function get_snippet_rtp()
end, vim.api.nvim_get_runtime_file("package.json", true))
end
-- sanitizes opts and returns ft -> files-map for `opts` (respects in/exclude).
-- sanitizes opts and returns
-- * ft -> files-map for `opts` (respects in/exclude).
-- * files -> ft-map (need to look up which filetypes a file contributes).
local function get_snippet_files(opts)
local paths
-- list of paths to crawl for loading (could be a table or a comma-separated-list)
@ -232,6 +249,7 @@ local function get_snippet_files(opts)
else
paths = opts.paths
end
paths = vim.tbl_map(Path.expand, paths) -- Expand before deduping, fake paths will become nil
paths = vim.tbl_filter(function(v)
return v
@ -251,39 +269,65 @@ local function get_snippet_files(opts)
return ft_paths
end
-- initializes ft_paths for `file`, and stores the add_opts for the filetype-file combination.
-- We can't just store add_opts for a single file, since via in/exclude, they
-- may differ for a single file which contributes multiple snippet-filetypes.
local function update_cache(cache, file, filetype, add_opts)
local filecache = cache.path_snippets[file]
if not filecache then
filecache = {
filetype_add_opts = {},
filetypes = {},
}
cache.path_snippets[file] = filecache
end
filecache.filetype_add_opts[filetype] = add_opts
filecache.filetypes[filetype] = true
end
local M = {}
function M.load(opts)
opts = opts or {}
-- applies in/exclude.
local ft_files = get_snippet_files(opts)
local add_opts = loader_util.add_opts(opts)
loader_util.extend_ft_paths(cache.ft_paths, ft_files)
loader_util.extend_ft_paths(package_cache.ft_paths, ft_files)
log.info("Loading snippet:", vim.inspect(ft_files))
for ft, files in pairs(ft_files) do
load_snippet_files(ft, files, add_opts)
for _, file in ipairs(files) do
update_cache(package_cache, file, ft, add_opts)
-- `false`: don't refresh while adding.
load_snippet_file(file, ft, add_opts, { refresh_notify = false })
end
ls.refresh_notify(ft)
end
end
function M._load_lazy_loaded_ft(ft)
for _, load_call_paths in ipairs(cache.lazy_load_paths) do
load_snippet_files(
for _, file in ipairs(package_cache.lazy_load_paths[ft] or {}) do
load_snippet_file(
file,
ft,
load_call_paths[ft] or {},
load_call_paths.add_opts
package_cache.path_snippets[file].filetype_add_opts[ft],
{ refresh_notify = false }
)
end
ls.refresh_notify(ft)
end
function M._load_lazy_loaded(bufnr)
local fts = loader_util.get_load_fts(bufnr)
for _, ft in ipairs(fts) do
if not cache.lazy_loaded_ft[ft] then
if not package_cache.lazy_loaded_ft[ft] then
M._load_lazy_loaded_ft(ft)
log.info("Loading lazy-load-snippets for filetype `%s`", ft)
cache.lazy_loaded_ft[ft] = true
package_cache.lazy_loaded_ft[ft] = true
end
end
end
@ -291,56 +335,124 @@ end
function M.lazy_load(opts)
opts = opts or {}
-- get two maps, one mapping filetype->associated files, and another
-- mapping files->default-filetypes.
local ft_files = get_snippet_files(opts)
local add_opts = loader_util.add_opts(opts)
loader_util.extend_ft_paths(cache.ft_paths, ft_files)
loader_util.extend_ft_paths(package_cache.ft_paths, ft_files)
-- immediately load filetypes that have already been loaded.
-- They will not be loaded otherwise.
for ft, files in pairs(ft_files) do
if cache.lazy_loaded_ft[ft] then
-- instantly load snippets if they were already loaded...
load_snippet_files(ft, files, add_opts)
log.info(
"Immediately loading lazy-load-snippets for already-active filetype %s from files:\n%s",
ft,
vim.inspect(files)
)
-- first register add_opts for all files, then iterate over files again
-- if they are already loaded.
for _, file in ipairs(files) do
update_cache(package_cache, file, ft, add_opts)
end
if package_cache.lazy_loaded_ft[ft] then
for _, file in ipairs(files) do
-- instantly load snippets if they were already loaded...
load_snippet_file(
file,
ft,
add_opts,
{ refresh_notify = false }
)
log.info(
"Immediately loading lazy-load-snippets for already-active filetype %s from files:\n%s",
ft,
vim.inspect(files)
)
end
ls.refresh_notify(ft)
-- don't load these files again.
-- clearing while iterating is fine: https://www.lua.org/manual/5.1/manual.html#pdf-next
ft_files[ft] = nil
end
end
log.info("Registering lazy-load-snippets:\n%s", vim.inspect(ft_files))
ft_files.add_opts = add_opts
table.insert(cache.lazy_load_paths, ft_files)
loader_util.extend_ft_paths(package_cache.lazy_load_paths, ft_files)
-- load for current buffer on startup.
M._load_lazy_loaded(vim.api.nvim_get_current_buf())
end
function M.edit_snippet_files()
loader_util.edit_snippet_files(cache.ft_paths)
loader_util.edit_snippet_files(package_cache.ft_paths)
end
-- Make sure filename is normalized.
function M._reload_file(filename)
local cached_data = cache.path_snippets[filename]
if not cached_data then
-- file is not loaded by this loader.
return
local function standalone_add(path, add_opts)
local file_snippets = get_file_snippets(path)
ls.add_snippets(
-- nil: provided snippets are a table mapping filetype->snippets.
"all",
file_snippets,
vim.tbl_extend("keep", {
key = string.format("__snippets_%s", path),
}, add_opts)
)
end
function M.load_standalone(opts)
opts = opts or {}
local path = Path.expand(opts.path)
local add_opts = loader_util.add_opts(opts)
-- register file for `all`-filetype in cache.
if not standalone_cache.ft_paths.all then
standalone_cache.ft_paths.all = {}
end
log.info("Re-loading snippets contributed by %s", filename)
cache.path_snippets[filename] = nil
local add_opts = cached_data.add_opts
-- record in cache, so edit_snippet_files can find it.
-- Store under "all" for now, alternative: collect all filetypes the
-- snippets contribute to.
-- Since .code-snippets are mainly (?) project-local, that behaviour does
-- not seem to bad.
table.insert(standalone_cache.ft_paths.all, path)
-- reload file for all filetypes it occurs in.
for ft, _ in pairs(cached_data.fts) do
load_snippet_files(ft, { filename }, add_opts)
-- only store add_opts, we don't need to remember filetypes and the like,
-- and here the filename is enough to identify add_opts.
standalone_cache.path_snippets[path] = add_opts
standalone_add(path, add_opts)
end
-- filename is normalized
function M._reload_file(filename)
local package_cached_data = package_cache.path_snippets[filename]
if package_cached_data then
log.info("Re-loading snippets contributed by %s", filename)
-- reload file for all filetypes it occurs in.
-- only the first call actually needs to force-reload, all other can
-- just use its snippets.
local force_reload = true
for ft, _ in pairs(package_cached_data.filetypes) do
load_snippet_file(
filename,
ft,
package_cached_data.filetype_add_opts[ft],
{ force_reload = force_reload }
)
-- only force-reload once, then reuse updated snippets.
force_reload = false
end
ls.clean_invalidated({ inv_limit = 100 })
end
local standalone_cached_data = standalone_cache.path_snippets[filename]
if standalone_cached_data then
log.info("Re-loading snippets contributed by %s", filename)
local add_opts = standalone_cached_data
standalone_add(filename, add_opts)
ls.clean_invalidated({ inv_limit = 100 })
end
end

View file

@ -1,9 +1,17 @@
local Cache = require("luasnip.loaders._caches")
local util = require("luasnip.util.util")
local loader_util = require("luasnip.loaders.util")
local Path = require("luasnip.util.path")
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"),
@ -38,7 +46,7 @@ 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()
local extend = opts.extend or function(_, _)
return {}
end
@ -48,9 +56,14 @@ function M.edit_snippet_files(opts)
local items = {}
-- concat files from all loaders for the selected filetype ft.
for _, cache_name in ipairs({ "vscode", "snipmate", "lua" }) do
for _, cache_name in ipairs({
"vscode_packages",
"vscode_standalone",
"snipmate",
"lua",
}) do
for _, path in ipairs(Cache[cache_name].ft_paths[ft] or {}) do
local fmt_name = format(path, cache_name)
local fmt_name = format(path, clean_name[cache_name])
if fmt_name then
table.insert(ft_paths, path)
table.insert(items, fmt_name)
@ -86,8 +99,16 @@ function M.edit_snippet_files(opts)
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(util.get_snippet_filetypes()) do
for _, ft in ipairs(all_fts) do
if ft_filter(ft) then
table.insert(filtered_fts, ft)
end

View file

@ -0,0 +1,70 @@
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()
return vim.tbl_map(M.duplicate_expandable, self.addable:retrieve_all())
end
local DupAddable_mt = {
__index = DupAddable,
}
function M.duplicate_addable(addable)
return setmetatable({
addable = addable,
}, DupAddable_mt)
end
return M

View file

@ -1,5 +1,6 @@
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 }
@ -8,7 +9,10 @@ function VirtualSnippet:get_docstring()
return self.snippet:get_docstring()
end
function VirtualSnippet:copy()
return self.snippet:copy()
local copy = self.snippet:copy()
copy.id = self.id
return copy
end
-- VirtualSnippet has all the fields for executing these methods.
@ -38,25 +42,13 @@ function MultiSnippet:retrieve_all()
return self.v_snips
end
local function new_multisnippet(contexts, nodes, opts)
local function multisnippet_from_snippet_obj(contexts, snippet, snippet_opts)
assert(
type(contexts) == "table",
"multisnippet: expected contexts to be a table."
)
opts = opts or {}
local common_snip_opts = opts.common_opts or {}
local common_context = node_util.wrap_context(contexts.common) or {}
-- create snippet without `context`-fields!
-- compare to `S` (aka `s`, the default snippet-constructor) in
-- `nodes/snippet.lua`.
local snippet = snip_mod._S(
snip_mod.init_snippet_opts(common_snip_opts),
nodes,
common_snip_opts
)
local v_snips = {}
for _, context in ipairs(contexts) do
local complete_context = vim.tbl_extend(
@ -66,7 +58,7 @@ local function new_multisnippet(contexts, nodes, opts)
)
table.insert(
v_snips,
new_virtual_snippet(complete_context, snippet, common_snip_opts)
new_virtual_snippet(complete_context, snippet, snippet_opts)
)
end
@ -79,6 +71,47 @@ local function new_multisnippet(contexts, nodes, opts)
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 = new_multisnippet,
new_multisnippet = multisnippet_from_nodes,
_raw_ms = multisnippet_from_snippet_obj,
}

View file

@ -189,6 +189,9 @@ local function init_snippet_context(context, opts)
or context.snippetType == "snippet" and "snippets"
or nil
-- may be nil.
effective_context.filetype = context.filetype
-- maybe do this in a better way when we have more parameters, but this is
-- fine for now:

View file

@ -47,12 +47,16 @@ 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 }
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 {}
@ -66,7 +70,12 @@ local function new(context, snippet, opts)
local sp = vim.tbl_extend(
"error",
{},
snip_mod.init_snippet_context(node_util.wrap_context(context), opts),
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)
)
@ -99,7 +108,10 @@ local function new(context, snippet, opts)
-- when the metatable has been changed. Therefore: set copy in each instance
-- of snippetProxy.
function sp:copy()
return self._snippet:copy()
local copy = self._snippet:copy()
copy.id = self.id
return copy
end
return sp

View file

@ -229,11 +229,11 @@ function M.clean_invalidated(opts)
M.invalidated_count = 0
end
local function invalidate_snippets(snippets_by_ft)
for _, ft_snippets in pairs(snippets_by_ft) do
for _, addable in ipairs(ft_snippets) do
for _, snip in ipairs(addable:retrieve_all()) do
snip:invalidate()
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
@ -247,26 +247,27 @@ 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
snip.priority = opts.override_priority
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
snip.snippetType = snip.snippetType ~= nil and snip.snippetType
local snip_type = snip.snippetType ~= nil and snip.snippetType
or opts.type
assert(
snip.snippetType == "autosnippets"
or snip.snippetType == "snippets",
"snipptType must be either 'autosnippets' or 'snippets'"
snip_type == "autosnippets" or snip_type == "snippets",
"snippetType must be either 'autosnippets' or 'snippets'"
)
local snip_ft = snip.filetype or ft
snip.id = current_id
current_id = current_id + 1
-- do the insertion
table.insert(by_prio[snip.snippetType][snip.priority][ft], snip)
table.insert(by_ft[snip.snippetType][ft], snip)
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 was passed, and remove from snippet.
@ -280,7 +281,7 @@ function M.add_snippets(snippets, opts)
if opts.key then
if by_key[opts.key] then
invalidate_snippets(by_key[opts.key])
invalidate_addables(by_key[opts.key])
end
by_key[opts.key] = snippets
end

View file

@ -31,6 +31,12 @@
"all"
],
"path": "snippets/all.jsonc"
},
{
"language": [
"all"
],
"path": "./snippets/all.code-snippets"
}
]
}

View file

@ -0,0 +1,10 @@
// uh oh, a comment.
{
"a": {
"prefix": "codesnippets",
"body": [
"code-snippets!!!"
]
}
/* oh no a block comment */
}

View file

@ -15,6 +15,13 @@
"luasnip": {
"priority": 2000
}
},
"c": {
"prefix": "cc",
"body": [
"3"
],
"scope": "cpp,c"
}
}

View file

@ -0,0 +1,36 @@
{
"snip1": {
"prefix": "vscode_lua1",
"body": [
"vscode$1lualualua"
],
"scope": "lua"
},
"snip1": {
"prefix": "all1",
"body": [
"expands? jumps? $1 $2 !"
],
"scope": "all"
},
"snip2": {
"prefix": "all2",
"body": [
"multi $1",
"line $2",
"#not removed??",
"snippet$0"
],
// empty scope should be all
},
"snip2": {
"prefix": "vscode_lua2",
"body": [
"vscode$1lualualua"
],
"luasnip": {
"autotrigger": true
},
"scope": "all"
}
}

View file

@ -154,6 +154,15 @@ M.loaders = {
)
)
end,
["vscode(standalone)"] = function()
exec_lua(
string.format(
[[require("luasnip.loaders.from_vscode").load_standalone({path="%s"})]],
os.getenv("LUASNIP_SOURCE")
.. "/tests/data/vscode-standalone.code-snippets"
)
)
end,
["snipmate(rtp)"] = function()
exec(

View file

@ -309,4 +309,30 @@ describe("add_snippets", function()
{2:-- INSERT --} |]],
})
end)
it("snippets' filetype overrides add_snippets-filetype", function()
exec_lua([[
ls.add_snippets("c", {
s({trig = "in_lua", filetype = "lua"}, {t"expanded in lua"})
})
]])
exec("set ft=c")
feed("iin_lua")
exec_lua("ls.expand()")
screen:expect({
grid = [[
in_lua^ |
{0:~ }|
{2:-- INSERT --} |]],
})
exec("set ft=lua")
feed("<Cr>in_lua")
exec_lua("ls.expand()")
screen:expect({
grid = [[
in_lua |
expanded in lua^ |
{2:-- INSERT --} |]],
})
end)
end)

View file

@ -388,6 +388,12 @@ describe("loaders:", function()
"/tests/data/vscode-snippets/snippets/all.json",
"<Esc>4jwlcereplaces<Esc>:w<Cr><C-O>ccall1"
)
reload_test(
"vscode-standalone-reload works",
ls_helpers.loaders["vscode(standalone)"],
"/tests/data/vscode-standalone.code-snippets",
"<Esc>11jwlcereplaces<Esc>:w<Cr><C-O>ccall1"
)
reload_test(
"lua-reload works",
@ -484,4 +490,70 @@ describe("loaders:", function()
"/tests/symlinked_data/lua-snippets/luasnippets/all.lua",
"<Esc>jfecereplaces<Esc>:w<Cr><C-O>ccall1"
)
it("Can load files with `code-snippets`-extension.", function()
ls_helpers.loaders["vscode(rtp)"]()
feed("icodesnippets")
exec_lua("ls.expand()")
screen:expect({
grid = [[
code-snippets!!!^ |
{0:~ }|
{0:~ }|
{0:~ }|
{2:-- INSERT --} |]],
})
end)
it("Respects `scope` (vscode)", function()
ls_helpers.loaders["vscode(rtp)"]()
feed("icc")
exec_lua("ls.expand()")
screen:expect({
grid = [[
cc^ |
{0:~ }|
{0:~ }|
{0:~ }|
{2:-- INSERT --} |]],
})
exec("set ft=c")
feed("<Cr>cc")
exec_lua("ls.expand()")
screen:expect({
grid = [[
cc |
3^ |
{0:~ }|
{0:~ }|
{2:-- INSERT --} |]],
})
-- check if invalidation affects the duplicated snippet.
exec_lua([[ls.get_snippets("c")[1]:invalidate()]])
feed("<Cr>cc")
exec_lua("ls.expand()")
screen:expect({
grid = [[
cc |
3 |
cc^ |
{0:~ }|
{2:-- INSERT --} |]],
})
exec("set ft=cpp")
feed("<Cr>cc")
exec_lua("ls.expand()")
screen:expect({
grid = [[
cc |
3 |
cc |
3^ |
{2:-- INSERT --} |]],
})
end)
end)

View file

@ -147,4 +147,36 @@ describe("multisnippets", function()
assert(ls.__did_expand)
]])
end)
it("work with extend_decorator", function()
ls_helpers.session_setup_luasnip({
setup_extend = { enable_autosnippets = true },
})
exec_lua([[
-- contexts without trigger get "asdf", add one context which has
-- the default-trigger and is an autosnippet.
local auto_multisnippet = ls.extend_decorator.apply(ls.multi_snippet, {common = "asdf", {snippetType = "autosnippet"}})
ls.add_snippets("all", {
auto_multisnippet({"bsdf"}, {t"csdf"})
}, {key = "asdf"})
]])
feed("iasdf")
screen:expect({
grid = [[
csdf^ |
{0:~ }|
{2:-- INSERT --} |]],
})
feed("<Cr>bsdf")
exec_lua("ls.expand()")
screen:expect({
grid = [[
csdf |
csdf^ |
{2:-- INSERT --} |]],
})
end)
end)

View file

@ -29,6 +29,7 @@ describe("snippetProxy", function()
".wordTrig",
".regTrig",
".dscr",
".filetype",
".name",
".callbacks",
".condition",