summaryrefslogtreecommitdiff
path: root/lua/blink/cmp/sources/snippets/default
diff options
context:
space:
mode:
authorMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
committerMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
commitb77413ff8f59f380612074f0c9bd49093d8db695 (patch)
tree32c39a811ba96ed4ab0a1c81cce9f8d518ed7e31 /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.lua188
-rw-r--r--lua/blink/cmp/sources/snippets/default/init.lua65
-rw-r--r--lua/blink/cmp/sources/snippets/default/registry.lua144
-rw-r--r--lua/blink/cmp/sources/snippets/default/scan.lua94
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