diff --git a/lua/lazyvim/plugins/extras/coding/mini-snippets.lua b/lua/lazyvim/plugins/extras/coding/mini-snippets.lua index d0a747a8..81c4d71c 100644 --- a/lua/lazyvim/plugins/extras/coding/mini-snippets.lua +++ b/lua/lazyvim/plugins/extras/coding/mini-snippets.lua @@ -7,15 +7,38 @@ if lazyvim_docs then -- https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-snippets.md#expand -- :h MiniSnippets-session + -- NOTE: To add your own snippets, you will need an extension manifest + -- in a runtime directory (e.g. ~/.config/nvim/package.json) + -- See https://github.com/rafamadriz/friendly-snippets/blob/main/package.json + -- for friendly-snippets' manifest + -- See https://code.visualstudio.com/api/references/extension-manifest + -- and https://code.visualstudio.com/api/references/contribution-points#contributes.snippets + -- for the original spec + -- + -- To add other snippets to a filetype, add them to opts.filetype_additions + -- To override a lang's filetypes, add an entry to opts.lang_filetypes + -- For example: + --[[ + return { + { + "echasnovski/mini.snippets", + opts = { + filetype_additions = { plaintext = { "license", "loremipsum" } }, + -- use "plaintext" snippets when context.lang is "text" + lang_filetypes = { text = { "plaintext" } }, + }, + }, + } + --]] + -- Example override for your own config: --[[ return { { "echasnovski/mini.snippets", opts = function(_, opts) - -- By default, for opts.snippets, the extra for mini.snippets only adds gen_loader.from_lang() + -- By default, for opts.snippets, the extra for mini.snippets only adds from_package_json() -- This provides a sensible quickstart, integrating with friendly-snippets - -- and your own language-specific snippets -- -- In order to change opts.snippets, replace the entire table inside your own opts @@ -27,7 +50,7 @@ if lazyvim_docs then -- Load snippets based on current language by reading files from -- "snippets/" subdirectories from 'runtimepath' directories. - snippets.gen_loader.from_lang(), -- this is the default in the extra... + snippets.gen_loader.from_lang(), } end, }, @@ -66,7 +89,7 @@ return { "echasnovski/mini.snippets", event = "InsertEnter", -- don't depend on other plugins to load... dependencies = "rafamadriz/friendly-snippets", - opts = function() + config = function(_, opts) ---@diagnostic disable-next-line: duplicate-set-field LazyVim.cmp.actions.snippet_stop = function() end -- by design, should not stop the session! ---@diagnostic disable-next-line: duplicate-set-field @@ -75,8 +98,175 @@ return { end local mini_snippets = require("mini.snippets") - return { - snippets = { mini_snippets.gen_loader.from_lang() }, + + local function from_package_json() + local filetype_additions, lang_filetypes = opts.filetype_additions or {}, opts.lang_filetypes or {} + + local cache = { filetype_snippets = {}, lang_snippets = {}, paths = nil } + + local function get_paths(filetype) + if cache.paths ~= nil then + cache.paths[filetype] = cache.paths[filetype] or {} + return cache.paths[filetype] + end + + cache.paths = {} + + local function insert_path(lang, root, path) + cache.paths[lang] = cache.paths[lang] or {} + table.insert(cache.paths[lang], vim.fs.joinpath(root, path)) + end + + local manifests = vim.api.nvim_get_runtime_file("package.json", true) + for _, manifest in ipairs(manifests) do + local function warn_of_manifest(msg) + LazyVim.warn({ "Could not parse", manifest, "", msg }, { title = "mini-snippets extra" }) + end + + if vim.fn.filereadable(manifest) ~= 1 then + warn_of_manifest("File is not readable") + goto continue + end + + local file, err = io.open(manifest) + + if file == nil then + warn_of_manifest(err) + goto continue + end + + local contents = file:read("*a") + + file:close() + + local ok, json = pcall(vim.json.decode, contents) + + if not ok then + warn_of_manifest(json) + goto continue + end + + if type(json) ~= "table" then + warn_of_manifest("Contents should be a table") + goto continue + end + + local snippets = vim.tbl_get(json, "contributes", "snippets") + + if snippets == nil then + goto continue + end + + if type(snippets) ~= "table" then + warn_of_manifest("`snippets` should be a table") + goto continue + end + + local i = 0 + for _ in pairs(snippets) do + i = i + 1 + + local snippet = snippets[i] + + if snippet == nil or type(snippet) ~= "table" then + warn_of_manifest("`snippets` should be a list of tables") + goto continue + end + + if type(snippet.path) ~= "string" then + warn_of_manifest("`path` of snippet #" .. i .. " should be a string") + goto continue + end + + local language = snippet.language + if type(language) == "table" then + local j = 0 + for _ in pairs(language) do + j = j + 1 + + if type(language[j]) ~= "string" then + warn_of_manifest( + "`language` of snippet #" + .. i + .. " should be a string, a list of strings, or nil for global snippets" + ) + goto continue + end + end + elseif not (type(language) == "string" or language == nil) then + warn_of_manifest( + "`language` of snippet #" .. i .. " should be a string, a list of strings, or nil for global snippets" + ) + goto continue + end + end + + local root = vim.fs.dirname(manifest) + + for _, snippet in ipairs(snippets) do + local language = snippet.language + local path = snippet.path + + if type(language) == "table" then + for _, ft in ipairs(language) do + insert_path(ft, root, path) + end + elseif type(language) == "string" then + insert_path(language, root, path) + else + insert_path("global", root, path) + end + end + + ::continue:: + end + + cache.paths[filetype] = cache.paths[filetype] or {} + return cache.paths[filetype] + end + + local function get_filetype_snippets(filetype) + if type(filetype) == "string" then + if cache.filetype_snippets[filetype] ~= nil then + return cache.filetype_snippets[filetype] + end + + local paths = {} + for _, ft in ipairs(filetype_additions[filetype] or {}) do + for _, path in ipairs(get_paths(ft)) do + table.insert(paths, path) + end + end + for _, path in ipairs(get_paths(filetype)) do + table.insert(paths, path) + end + + cache.filetype_snippets[filetype] = vim.tbl_map(mini_snippets.read_file, paths) + return cache.filetype_snippets[filetype] + end + + return vim.tbl_map(get_filetype_snippets, filetype) + end + + local function get_lang_snippets(lang) + cache.lang_snippets[lang] = cache.lang_snippets[lang] + or get_filetype_snippets(lang_filetypes[lang] or { vim.treesitter.language.get_filetypes(lang), "global" }) + return cache.lang_snippets[lang] + end + + return function(context) + local lang = (context or {}).lang + + if type(lang) ~= "string" then + return get_lang_snippets("global") + end + + return get_lang_snippets(lang) + end + end + + opts = vim.tbl_deep_extend("force", { + snippets = { from_package_json() }, -- Following the behavior of vim.snippets, -- the intended usage of is to be able to temporarily exit into normal mode for quick edits. @@ -92,7 +282,9 @@ return { select(snippets, insert) end, }, - } + }, opts) + + mini_snippets.setup(opts) end, },