diff options
| author | Mike Vink <mike@pionative.com> | 2025-01-19 13:52:52 +0100 |
|---|---|---|
| committer | Mike Vink <mike@pionative.com> | 2025-01-19 13:52:52 +0100 |
| commit | b77413ff8f59f380612074f0c9bd49093d8db695 (patch) | |
| tree | 32c39a811ba96ed4ab0a1c81cce9f8d518ed7e31 /lua/blink/cmp/sources/snippets/default | |
Squashed 'mut/neovim/pack/plugins/start/blink.cmp/' content from commit 1cc3b1a
git-subtree-dir: mut/neovim/pack/plugins/start/blink.cmp
git-subtree-split: 1cc3b1a908fbcfd15451c4772759549724f38524
Diffstat (limited to 'lua/blink/cmp/sources/snippets/default')
| -rw-r--r-- | lua/blink/cmp/sources/snippets/default/builtin.lua | 188 | ||||
| -rw-r--r-- | lua/blink/cmp/sources/snippets/default/init.lua | 65 | ||||
| -rw-r--r-- | lua/blink/cmp/sources/snippets/default/registry.lua | 144 | ||||
| -rw-r--r-- | lua/blink/cmp/sources/snippets/default/scan.lua | 94 |
4 files changed, 491 insertions, 0 deletions
diff --git a/lua/blink/cmp/sources/snippets/default/builtin.lua b/lua/blink/cmp/sources/snippets/default/builtin.lua new file mode 100644 index 0000000..b66ca1c --- /dev/null +++ b/lua/blink/cmp/sources/snippets/default/builtin.lua @@ -0,0 +1,188 @@ +-- credit to https://github.com/L3MON4D3 for these variables +-- see: https://github.com/L3MON4D3/LuaSnip/blob/master/lua/luasnip/util/_builtin_vars.lua +-- and credit to https://github.com/garymjr for his changes +-- see: https://github.com/garymjr/nvim-snippets/blob/main/lua/snippets/utils/builtin.lua + +local builtin = { + lazy = {}, +} + +function builtin.lazy.TM_FILENAME() return vim.fn.expand('%:t') end + +function builtin.lazy.TM_FILENAME_BASE() return vim.fn.expand('%:t:s?\\.[^\\.]\\+$??') end + +function builtin.lazy.TM_DIRECTORY() return vim.fn.expand('%:p:h') end + +function builtin.lazy.TM_FILEPATH() return vim.fn.expand('%:p') end + +function builtin.lazy.CLIPBOARD(opts) return vim.fn.getreg(opts.clipboard_register or vim.v.register, 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 builtin.lazy.RELATIVE_FILEPATH() -- The relative (to the opened workspace or folder) file path of the current document + return buf_to_ws_part()[2] +end + +function builtin.lazy.WORKSPACE_FOLDER() -- The path of the opened workspace or folder + return buf_to_ws_part()[1] +end + +function builtin.lazy.WORKSPACE_NAME() -- The name of the opened workspace or folder + local parts = vim.split(buf_to_ws_part()[1] or '', '[\\/]') + return parts[#parts] +end + +function builtin.lazy.CURRENT_YEAR() return os.date('%Y') end + +function builtin.lazy.CURRENT_YEAR_SHORT() return os.date('%y') end + +function builtin.lazy.CURRENT_MONTH() return os.date('%m') end + +function builtin.lazy.CURRENT_MONTH_NAME() return os.date('%B') end + +function builtin.lazy.CURRENT_MONTH_NAME_SHORT() return os.date('%b') end + +function builtin.lazy.CURRENT_DATE() return os.date('%d') end + +function builtin.lazy.CURRENT_DAY_NAME() return os.date('%A') end + +function builtin.lazy.CURRENT_DAY_NAME_SHORT() return os.date('%a') end + +function builtin.lazy.CURRENT_HOUR() return os.date('%H') end + +function builtin.lazy.CURRENT_MINUTE() return os.date('%M') end + +function builtin.lazy.CURRENT_SECOND() return os.date('%S') end + +function builtin.lazy.CURRENT_SECONDS_UNIX() return tostring(os.time()) end + +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 + +function builtin.lazy.CURRENT_TIMEZONE_OFFSET() + return get_timezone_offset(os.time()):gsub('([+-])(%d%d)(%d%d)$', '%1%2:%3') +end + +math.randomseed(os.time()) + +function builtin.lazy.RANDOM() return string.format('%06d', math.random(999999)) end + +function builtin.lazy.RANDOM_HEX() + return string.format('%06x', math.random(16777216)) --16^6 +end + +function builtin.lazy.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 + +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 + +function builtin.lazy.LINE_COMMENT() return buffer_comment_chars()[1] end + +function builtin.lazy.BLOCK_COMMENT_START() return buffer_comment_chars()[2] end + +function builtin.lazy.BLOCK_COMMENT_END() return buffer_comment_chars()[3] end + +local function get_cursor() + local c = vim.api.nvim_win_get_cursor(0) + c[1] = c[1] - 1 + return c +end + +local function get_current_line() + local pos = get_cursor() + return vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1] +end + +local function word_under_cursor(cur, line) + if line == nil then return end + + 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 + +local function get_selected_text() + if vim.fn.visualmode() == 'V' then return vim.fn.trim(vim.fn.getreg(vim.v.register, true), '\n', 2) end + return '' +end + +vim.api.nvim_create_autocmd('InsertEnter', { + group = vim.api.nvim_create_augroup('BlinkSnippetsEagerEnter', { clear = true }), + callback = function() + builtin.eager = {} + builtin.eager.TM_CURRENT_LINE = get_current_line() + builtin.eager.TM_CURRENT_WORD = word_under_cursor(get_cursor(), builtin.eager.TM_CURRENT_LINE) + builtin.eager.TM_LINE_INDEX = tostring(get_cursor()[1]) + builtin.eager.TM_LINE_NUMBER = tostring(get_cursor()[1] + 1) + builtin.eager.TM_SELECTED_TEXT = get_selected_text() + end, +}) + +vim.api.nvim_create_autocmd('InsertLeave', { + group = vim.api.nvim_create_augroup('BlinkSnippetsEagerLeave', { clear = true }), + callback = function() builtin.eager = nil end, +}) + +return builtin diff --git a/lua/blink/cmp/sources/snippets/default/init.lua b/lua/blink/cmp/sources/snippets/default/init.lua new file mode 100644 index 0000000..db7fece --- /dev/null +++ b/lua/blink/cmp/sources/snippets/default/init.lua @@ -0,0 +1,65 @@ +--- @class blink.cmp.SnippetsOpts +--- @field friendly_snippets? boolean +--- @field search_paths? string[] +--- @field global_snippets? string[] +--- @field extended_filetypes? table<string, string[]> +--- @field ignored_filetypes? string[] +--- @field get_filetype? fun(context: blink.cmp.Context): string +--- @field clipboard_register? string + +local snippets = {} + +function snippets.new(opts) + --- @cast opts blink.cmp.SnippetsOpts + + local self = setmetatable({}, { __index = snippets }) + --- @type table<string, blink.cmp.CompletionItem[]> + self.cache = {} + self.registry = require('blink.cmp.sources.snippets.default.registry').new(opts) + self.get_filetype = opts.get_filetype or function() return vim.bo.filetype end + return self +end + +function snippets:get_completions(context, callback) + local filetype = self.get_filetype(context) + if vim.tbl_contains(self.registry.config.ignored_filetypes, filetype) then return callback() end + + if not self.cache[filetype] then + local global_snippets = self.registry:get_global_snippets() + local extended_snippets = self.registry:get_extended_snippets(filetype) + local ft_snippets = self.registry:get_snippets_for_ft(filetype) + local snips = vim.list_extend({}, global_snippets) + vim.list_extend(snips, extended_snippets) + vim.list_extend(snips, ft_snippets) + + self.cache[filetype] = snips + end + + local items = vim.tbl_map( + function(item) return self.registry:snippet_to_completion_item(item) end, + self.cache[filetype] + ) + callback({ + is_incomplete_forward = false, + is_incomplete_backward = false, + items = items, + }) +end + +function snippets:resolve(item, callback) + local parsed_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(item.insertText) + local snippet = parsed_snippet and tostring(parsed_snippet) or item.insertText + + local resolved_item = vim.deepcopy(item) + resolved_item.detail = snippet + resolved_item.documentation = { + kind = 'markdown', + value = item.description, + } + callback(resolved_item) +end + +--- For external integrations to force reloading the snippets +function snippets:reload() self.cache = {} end + +return snippets diff --git a/lua/blink/cmp/sources/snippets/default/registry.lua b/lua/blink/cmp/sources/snippets/default/registry.lua new file mode 100644 index 0000000..5be225c --- /dev/null +++ b/lua/blink/cmp/sources/snippets/default/registry.lua @@ -0,0 +1,144 @@ +--- Credit to https://github.com/garymjr/nvim-snippets/blob/main/lua/snippets/utils/init.lua +--- for the original implementation +--- Original License: MIT + +--- @class blink.cmp.Snippet +--- @field prefix string +--- @field body string[] | string +--- @field description? string + +local registry = { + builtin_vars = require('blink.cmp.sources.snippets.default.builtin'), +} + +local utils = require('blink.cmp.sources.snippets.utils') +local default_config = { + friendly_snippets = true, + search_paths = { vim.fn.stdpath('config') .. '/snippets' }, + global_snippets = { 'all' }, + extended_filetypes = {}, + ignored_filetypes = {}, + --- @type string? + clipboard_register = nil, +} + +--- @param config blink.cmp.SnippetsOpts +function registry.new(config) + local self = setmetatable({}, { __index = registry }) + self.config = vim.tbl_deep_extend('force', default_config, config) + self.config.search_paths = vim.tbl_map(function(path) return vim.fs.normalize(path) end, self.config.search_paths) + + if self.config.friendly_snippets then + for _, path in ipairs(vim.api.nvim_list_runtime_paths()) do + if string.match(path, 'friendly.snippets') then table.insert(self.config.search_paths, path) end + end + end + self.registry = require('blink.cmp.sources.snippets.default.scan').register_snippets(self.config.search_paths) + + return self +end + +--- @param filetype string +--- @return blink.cmp.Snippet[] +function registry:get_snippets_for_ft(filetype) + local loaded_snippets = {} + local files = self.registry[filetype] + if not files then return loaded_snippets end + + files = type(files) == 'table' and files or { files } + + for _, f in ipairs(files) do + local contents = utils.read_file(f) + if contents then + local snippets = utils.parse_json_with_error_msg(f, contents) + for _, key in ipairs(vim.tbl_keys(snippets)) do + local snippet = utils.read_snippet(snippets[key], key) + for _, snippet_def in pairs(snippet) do + table.insert(loaded_snippets, snippet_def) + end + end + end + end + + return loaded_snippets +end + +--- @param filetype string +--- @return blink.cmp.Snippet[] +function registry:get_extended_snippets(filetype) + local loaded_snippets = {} + if not filetype then return loaded_snippets end + + local extended_snippets = self.config.extended_filetypes[filetype] or {} + for _, ft in ipairs(extended_snippets) do + if vim.tbl_contains(self.config.extended_filetypes, filetype) then + vim.list_extend(loaded_snippets, self:get_extended_snippets(ft)) + else + vim.list_extend(loaded_snippets, self:get_snippets_for_ft(ft)) + end + end + return loaded_snippets +end + +--- @return blink.cmp.Snippet[] +function registry:get_global_snippets() + local loaded_snippets = {} + local global_snippets = self.config.global_snippets + for _, ft in ipairs(global_snippets) do + if vim.tbl_contains(self.config.extended_filetypes, ft) then + vim.list_extend(loaded_snippets, self:get_extended_snippets(ft)) + else + vim.list_extend(loaded_snippets, self:get_snippets_for_ft(ft)) + end + end + return loaded_snippets +end + +--- @param snippet blink.cmp.Snippet +--- @return blink.cmp.CompletionItem +function registry:snippet_to_completion_item(snippet) + local body = type(snippet.body) == 'string' and snippet.body or table.concat(snippet.body, '\n') + return { + kind = require('blink.cmp.types').CompletionItemKind.Snippet, + label = snippet.prefix, + insertTextFormat = vim.lsp.protocol.InsertTextFormat.Snippet, + insertText = self:expand_vars(body), + description = snippet.description, + } +end + +--- @param snippet string +--- @return string +function registry:parse_body(snippet) + local parse = utils.safe_parse(self:expand_vars(snippet)) + return parse and tostring(parse) or snippet +end + +--- @param snippet string +--- @return string +function registry:expand_vars(snippet) + local lazy_vars = self.builtin_vars.lazy + local eager_vars = self.builtin_vars.eager or {} + + local resolved_snippet = snippet + local parsed_snippet = utils.safe_parse(snippet) + if not parsed_snippet then return snippet end + + for _, child in ipairs(parsed_snippet.data.children) do + local type, data = child.type, child.data + if type == vim.lsp._snippet_grammar.NodeType.Variable then + if eager_vars[data.name] then + resolved_snippet = resolved_snippet:gsub('%$[{]?(' .. data.name .. ')[}]?', eager_vars[data.name]) + elseif lazy_vars[data.name] then + resolved_snippet = resolved_snippet:gsub( + '%$[{]?(' .. data.name .. ')[}]?', + lazy_vars[data.name]({ clipboard_register = self.config.clipboard_register }) + ) + end + end + end + + return resolved_snippet +end + +return registry diff --git a/lua/blink/cmp/sources/snippets/default/scan.lua b/lua/blink/cmp/sources/snippets/default/scan.lua new file mode 100644 index 0000000..6971691 --- /dev/null +++ b/lua/blink/cmp/sources/snippets/default/scan.lua @@ -0,0 +1,94 @@ +local utils = require('blink.cmp.sources.snippets.utils') +local scan = {} + +function scan.register_snippets(search_paths) + local registry = {} + + for _, path in ipairs(search_paths) do + local files = scan.load_package_json(path) or scan.scan_for_snippets(path) + for ft, file in pairs(files) do + local key + if type(ft) == 'number' then + key = vim.fn.fnamemodify(files[ft], ':t:r') + else + key = ft + end + + if not key then return end + + registry[key] = registry[key] or {} + if type(file) == 'table' then + vim.list_extend(registry[key], file) + else + table.insert(registry[key], file) + end + end + end + + return registry +end + +---@type fun(self: utils, dir: string, result?: string[]): string[] +---@return string[] +function scan.scan_for_snippets(dir, result) + result = result or {} + + local stat = vim.uv.fs_stat(dir) + if not stat then return result end + + if stat.type == 'directory' then + local req = vim.uv.fs_scandir(dir) + if not req then return result end + + local function iter() return vim.uv.fs_scandir_next(req) end + + for name, ftype in iter do + local path = string.format('%s/%s', dir, name) + + if ftype == 'directory' then + result[name] = scan.scan_for_snippets(path, result[name] or {}) + else + scan.scan_for_snippets(path, result) + end + end + elseif stat.type == 'file' then + local name = vim.fn.fnamemodify(dir, ':t') + + if name:match('%.json$') then table.insert(result, dir) end + elseif stat.type == 'link' then + local target = vim.uv.fs_readlink(dir) + + if target then scan.scan_for_snippets(target, result) end + end + + return result +end + +--- This will try to load the snippets from the package.json file +---@param path string +function scan.load_package_json(path) + local file = path .. '/package.json' + -- todo: ideally this is async, although it takes 0.5ms on my system so it might not matter + local data = utils.read_file(file) + if not data then return end + + local pkg = require('blink.cmp.sources.snippets.utils').parse_json_with_error_msg(file, data) + + ---@type {path: string, language: string|string[]}[] + local snippets = vim.tbl_get(pkg, 'contributes', 'snippets') + if not snippets then return end + + local ret = {} ---@type table<string, string[]> + for _, s in ipairs(snippets) do + local langs = s.language or {} + langs = type(langs) == 'string' and { langs } or langs + ---@cast langs string[] + for _, lang in ipairs(langs) do + ret[lang] = ret[lang] or {} + table.insert(ret[lang], vim.fs.normalize(vim.fs.joinpath(path, s.path))) + end + end + return ret +end + +return scan |
