From 6b5f496983ab718d7e9441ddd8a3c0b724ac6db8 Mon Sep 17 00:00:00 2001 From: Soifou Date: Sat, 1 Nov 2025 14:16:45 +0100 Subject: [PATCH 01/13] chore(luasnip): add missing annotations --- lua/blink/cmp/sources/snippets/luasnip.lua | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index dfdfff6c..22cb4066 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -7,13 +7,11 @@ --- @class blink.cmp.LuasnipSource : blink.cmp.Source --- @field config blink.cmp.LuasnipSourceOptions --- @field items_cache table +local source = {} local utils = require('blink.cmp.lib.utils') ---- @type blink.cmp.LuasnipSource ---- @diagnostic disable-next-line: missing-fields -local source = {} - +--- @type blink.cmp.LuasnipSourceOptions local default_config = { use_show_condition = true, show_autosnippets = true, @@ -146,13 +144,16 @@ function source:resolve(item, callback) local resolved_item = vim.deepcopy(item) + ---@type string|string[]|nil local detail = snip:get_docstring() if type(detail) == 'table' then detail = table.concat(detail, '\n') end resolved_item.detail = detail + ---@diagnostic disable-next-line: undefined-field if snip.dscr then resolved_item.documentation = { kind = 'markdown', + ---@diagnostic disable-next-line: undefined-field value = table.concat(vim.lsp.util.convert_input_to_markdown_lines(snip.dscr), '\n'), } end @@ -165,9 +166,11 @@ function source:execute(ctx, item) local snip = luasnip.get_id_snippet(item.data.snip_id) -- if trigger is a pattern, expand "pattern" instead of actual snippet + ---@diagnostic disable-next-line: undefined-field if snip.regTrig then + ---@diagnostic disable-next-line: undefined-field local docTrig = self.config.prefer_doc_trig and snip.docTrig - snip = snip:get_pattern_expand_helper() + snip = snip:get_pattern_expand_helper() --[[@as LuaSnip.Snippet]] if docTrig then add_luasnip_callback(snip, 'pre_expand', function(snip, _) From d6225ae5848d4e7c17b21e9900746deb66c50450 Mon Sep 17 00:00:00 2001 From: Soifou Date: Sat, 1 Nov 2025 14:24:42 +0100 Subject: [PATCH 02/13] perf(luasnip): reduce table lookups --- lua/blink/cmp/sources/snippets/luasnip.lua | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index 22cb4066..401e139e 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -1,3 +1,9 @@ +---@type LuaSnip.API +local luasnip +local utils = require('blink.cmp.lib.utils') +local text_edits = require('blink.cmp.lib.text_edits') +local kind_snippet = require('blink.cmp.types').CompletionItemKind.Snippet + --- @class blink.cmp.LuasnipSourceOptions --- @field use_show_condition? boolean Whether to use show_condition for filtering snippets --- @field show_autosnippets? boolean Whether to show autosnippets in the completion list @@ -9,8 +15,6 @@ --- @field items_cache table local source = {} -local utils = require('blink.cmp.lib.utils') - --- @type blink.cmp.LuasnipSourceOptions local default_config = { use_show_condition = true, @@ -61,7 +65,8 @@ function source.new(opts) end function source:enabled() - local ok, _ = pcall(require, 'luasnip') + local ok, mod = pcall(require, 'luasnip') + if ok then luasnip = mod end return ok end @@ -81,13 +86,13 @@ function source:get_completions(ctx, callback) -- cache not yet available for this filetype self.items_cache[ft] = {} -- Gather filetype snippets and, optionally, autosnippets - local snippets = require('luasnip').get_snippets(ft, { type = 'snippets' }) + local snippets = luasnip.get_snippets(ft, { type = 'snippets' }) if self.config.show_autosnippets then - local autosnippets = require('luasnip').get_snippets(ft, { type = 'autosnippets' }) + local autosnippets = luasnip.get_snippets(ft, { type = 'autosnippets' }) for _, s in ipairs(autosnippets) do add_luasnip_callback(s, 'enter', require('blink.cmp').hide) end - snippets = require('blink.cmp.lib.utils').shallow_copy(snippets) + snippets = utils.shallow_copy(snippets) vim.list_extend(snippets, autosnippets) end snippets = vim.tbl_filter(function(snip) return not snip.hidden end, snippets) @@ -106,7 +111,7 @@ function source:get_completions(ctx, callback) --- @type lsp.CompletionItem local item = { - kind = require('blink.cmp.types').CompletionItemKind.Snippet, + kind = kind_snippet, label = snip.regTrig and snip.name or snip.trigger, insertText = self.config.prefer_doc_trig and snip.docTrig or snip.trigger, insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText, @@ -140,7 +145,7 @@ function source:get_completions(ctx, callback) end function source:resolve(item, callback) - local snip = require('luasnip').get_id_snippet(item.data.snip_id) + local snip = luasnip.get_id_snippet(item.data.snip_id) local resolved_item = vim.deepcopy(item) @@ -162,7 +167,6 @@ function source:resolve(item, callback) end function source:execute(ctx, item) - local luasnip = require('luasnip') local snip = luasnip.get_id_snippet(item.data.snip_id) -- if trigger is a pattern, expand "pattern" instead of actual snippet @@ -191,7 +195,7 @@ function source:execute(ctx, item) local cursor = ctx.get_cursor() cursor[1] = cursor[1] - 1 - local range = require('blink.cmp.lib.text_edits').get_from_item(item).range + local range = text_edits.get_from_item(item).range local clear_region = { from = { range.start.line, range.start.character }, to = cursor, From 0b975692994c5cc99a7fccab8df78c9757a2f04a Mon Sep 17 00:00:00 2001 From: Soifou Date: Sat, 1 Nov 2025 17:42:09 +0100 Subject: [PATCH 03/13] refactor(luasnip): rename config to opts --- lua/blink/cmp/sources/snippets/luasnip.lua | 43 +++++++++++----------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index 401e139e..55dcc700 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -11,18 +11,10 @@ local kind_snippet = require('blink.cmp.types').CompletionItemKind.Snippet --- @field use_label_description? boolean Whether to put the snippet description in the label description --- @class blink.cmp.LuasnipSource : blink.cmp.Source ---- @field config blink.cmp.LuasnipSourceOptions +--- @field opts blink.cmp.LuasnipSourceOptions --- @field items_cache table local source = {} ---- @type blink.cmp.LuasnipSourceOptions -local default_config = { - use_show_condition = true, - show_autosnippets = true, - prefer_doc_trig = false, - use_label_description = false, -} - ---@param snippet table ---@param event string ---@param callback fun(table, table) @@ -34,17 +26,24 @@ local function add_luasnip_callback(snippet, event, callback) snippet.callbacks[-1][events[event]] = callback end +---@param opts blink.cmp.LuasnipSourceOptions function source.new(opts) - local config = vim.tbl_deep_extend('keep', opts, default_config) + local self = setmetatable({}, { __index = source }) + + opts = vim.tbl_deep_extend('keep', opts or {}, { + use_show_condition = true, + show_autosnippets = true, + prefer_doc_trig = false, + use_label_description = false, + }) require('blink.cmp.config.utils').validate('sources.providers.snippets.opts', { - use_show_condition = { config.use_show_condition, 'boolean' }, - show_autosnippets = { config.show_autosnippets, 'boolean' }, - prefer_doc_trig = { config.prefer_doc_trig, 'boolean' }, - use_label_description = { config.use_label_description, 'boolean' }, - }, config) + use_show_condition = { opts.use_show_condition, 'boolean' }, + show_autosnippets = { opts.show_autosnippets, 'boolean' }, + prefer_doc_trig = { opts.prefer_doc_trig, 'boolean' }, + use_label_description = { opts.use_label_description, 'boolean' }, + }, opts) - local self = setmetatable({}, { __index = source }) - self.config = config + self.opts = opts self.items_cache = {} local luasnip_ag = vim.api.nvim_create_augroup('BlinkCmpLuaSnipReload', { clear = true }) @@ -87,7 +86,7 @@ function source:get_completions(ctx, callback) self.items_cache[ft] = {} -- Gather filetype snippets and, optionally, autosnippets local snippets = luasnip.get_snippets(ft, { type = 'snippets' }) - if self.config.show_autosnippets then + if self.opts.show_autosnippets then local autosnippets = luasnip.get_snippets(ft, { type = 'autosnippets' }) for _, s in ipairs(autosnippets) do add_luasnip_callback(s, 'enter', require('blink.cmp').hide) @@ -113,11 +112,11 @@ function source:get_completions(ctx, callback) local item = { kind = kind_snippet, label = snip.regTrig and snip.name or snip.trigger, - insertText = self.config.prefer_doc_trig and snip.docTrig or snip.trigger, + insertText = self.opts.prefer_doc_trig and snip.docTrig or snip.trigger, insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText, sortText = sort_text, data = { snip_id = snip.id, show_condition = snip.show_condition }, - labelDetails = snip.dscr and self.config.use_label_description and { + labelDetails = snip.dscr and self.opts.use_label_description and { description = table.concat(snip.dscr, ' '), } or nil, } @@ -131,7 +130,7 @@ function source:get_completions(ctx, callback) end -- Filter items based on show_condition, if configured - if self.config.use_show_condition then + if self.opts.use_show_condition then local line_to_cursor = ctx.line:sub(0, ctx.cursor[2] - 1) items = vim.tbl_filter(function(item) return item.data.show_condition(line_to_cursor) end, items) end @@ -173,7 +172,7 @@ function source:execute(ctx, item) ---@diagnostic disable-next-line: undefined-field if snip.regTrig then ---@diagnostic disable-next-line: undefined-field - local docTrig = self.config.prefer_doc_trig and snip.docTrig + local docTrig = self.opts.prefer_doc_trig and snip.docTrig snip = snip:get_pattern_expand_helper() --[[@as LuaSnip.Snippet]] if docTrig then From cb585040747ebcd8c01103c59d04283b6935fe12 Mon Sep 17 00:00:00 2001 From: Soifou Date: Sat, 1 Nov 2025 18:11:23 +0100 Subject: [PATCH 04/13] chore(luasnip): add more annotations --- lua/blink/cmp/sources/snippets/luasnip.lua | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index 55dcc700..b67224c0 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -69,6 +69,8 @@ function source:enabled() return ok end +---@param ctx blink.cmp.Context +---@param callback fun(result?: blink.cmp.CompletionResponse) function source:get_completions(ctx, callback) --- @type blink.cmp.CompletionItem[] local items = {} @@ -165,6 +167,8 @@ function source:resolve(item, callback) callback(resolved_item) end +---@param ctx blink.cmp.Context +---@param item blink.cmp.CompletionItem function source:execute(ctx, item) local snip = luasnip.get_id_snippet(item.data.snip_id) @@ -190,11 +194,11 @@ function source:execute(ctx, item) end end - -- get (0, 0) indexed cursor position - local cursor = ctx.get_cursor() + local cursor = ctx.get_cursor() --[[@as LuaSnip.BytecolBufferPosition]] cursor[1] = cursor[1] - 1 local range = text_edits.get_from_item(item).range + ---@type LuaSnip.BufferRegion local clear_region = { from = { range.start.line, range.start.character }, to = cursor, @@ -204,18 +208,19 @@ function source:execute(ctx, item) local line_to_cursor = line:sub(1, cursor[2]) local range_text = line:sub(range.start.character + 1, cursor[2]) + ---@type LuaSnip.Opts.SnipExpandExpandParams? local expand_params = snip:matches(line_to_cursor, { fallback_match = range_text ~= line_to_cursor and range_text, }) if expand_params ~= nil then + ---@diagnostic disable-next-line: undefined-field if expand_params.clear_region ~= nil then + ---@diagnostic disable-next-line: undefined-field clear_region = expand_params.clear_region elseif expand_params.trigger ~= nil then - clear_region = { - from = { cursor[1], cursor[2] - #expand_params.trigger }, - to = cursor, - } + clear_region.from = { cursor[1], cursor[2] - #expand_params.trigger } + clear_region.to = cursor end end From 7be3f4367491dd46083a0377a3709a2f19b04100 Mon Sep 17 00:00:00 2001 From: Soifou Date: Sat, 1 Nov 2025 18:18:18 +0100 Subject: [PATCH 05/13] chore(luasnip): drop useless return value --- lua/blink/cmp/sources/snippets/luasnip.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index b67224c0..71684c03 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -141,7 +141,6 @@ function source:get_completions(ctx, callback) is_incomplete_forward = false, is_incomplete_backward = false, items = items, - context = ctx, }) end From 28a41935c17754db9136a3eb5b1e0c7b4a42e44a Mon Sep 17 00:00:00 2001 From: Soifou Date: Sat, 1 Nov 2025 22:45:19 +0100 Subject: [PATCH 06/13] fix(luasnip): prevent potential race conditions --- lua/blink/cmp/sources/snippets/luasnip.lua | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index 71684c03..03532429 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -75,17 +75,18 @@ function source:get_completions(ctx, callback) --- @type blink.cmp.CompletionItem[] local items = {} - -- gather snippets from relevant filetypes, including extensions + -- Gather snippets from relevant filetypes, including extensions for _, ft in ipairs(require('luasnip.util.util').get_snippet_filetypes()) do - if self.items_cache[ft] then + if self.items_cache[ft] and #self.items_cache[ft] > 0 then for _, item in ipairs(self.items_cache[ft]) do table.insert(items, utils.shallow_copy(item)) end goto continue end - -- cache not yet available for this filetype - self.items_cache[ft] = {} + -- Cache not yet available for this filetype + self.items_cache[ft] = nil + -- Gather filetype snippets and, optionally, autosnippets local snippets = luasnip.get_snippets(ft, { type = 'snippets' }) if self.opts.show_autosnippets then @@ -104,6 +105,7 @@ function source:get_completions(ctx, callback) max_priority = math.max(max_priority, snip.effective_priority or 0) end + local ft_items = {} for _, snip in ipairs(snippets) do -- Convert priority of 1000 (with max of 8000) to string like "00007000|||asd" for sorting -- This will put high priority snippets at the top of the list, and break ties based on the trigger @@ -122,12 +124,14 @@ function source:get_completions(ctx, callback) description = table.concat(snip.dscr, ' '), } or nil, } - -- populate snippet cache for this filetype - table.insert(self.items_cache[ft], item) - -- while we're at it, also populate completion items for this request + -- Populate snippet cache for this filetype + table.insert(ft_items, item) + -- While we're at it, also populate completion items for this request table.insert(items, utils.shallow_copy(item)) end + self.items_cache[ft] = ft_items + ::continue:: end From 7d7e03bbb54303af2527798a33fcf6f225ed7177 Mon Sep 17 00:00:00 2001 From: Soifou Date: Mon, 3 Nov 2025 18:04:52 +0100 Subject: [PATCH 07/13] chore: remove obsolete luasnip check blink.cmp v1.0.0 has been release 8 months ago, this check can be safely removed now. --- lua/blink/cmp/sources/lib/init.lua | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lua/blink/cmp/sources/lib/init.lua b/lua/blink/cmp/sources/lib/init.lua index e008a3f3..16c1e40d 100644 --- a/lua/blink/cmp/sources/lib/init.lua +++ b/lua/blink/cmp/sources/lib/init.lua @@ -107,13 +107,6 @@ function sources.get_enabled_providers(mode) end function sources.get_provider_by_id(provider_id) - -- TODO: remove in v1.0 - if not sources.providers[provider_id] and provider_id == 'luasnip' then - error( - "Luasnip has been moved to the `snippets` source, alongside a new preset system (`snippets.preset = 'luasnip'`). See the documentation for more information." - ) - end - assert( sources.providers[provider_id] ~= nil or config.sources.providers[provider_id] ~= nil, 'Requested provider "' From 3fc18153d6f1d59e04941b790e720f71e4b8329d Mon Sep 17 00:00:00 2001 From: Soifou Date: Mon, 3 Nov 2025 21:01:26 +0100 Subject: [PATCH 08/13] feat(luasnip): enhance `insertText` with static text from textNodes Instead of displaying only `trigger`, first try to generate the preview from the snippet's textNodes; if that's not available, fall back to `docTrig` if it exists, or use the `trigger` as a last resort. --- lua/blink/cmp/sources/snippets/luasnip.lua | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index 03532429..d8434022 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -26,6 +26,28 @@ local function add_luasnip_callback(snippet, event, callback) snippet.callbacks[-1][events[event]] = callback end +---@param snippet LuaSnip.Snippet +---@return string? +local function get_insert_text(snippet) + local res = {} + for _, node in ipairs(snippet.nodes) do + ---@cast node LuaSnip.Node + -- TODO: How to know the node type? Would be nice to handle the others as well + -- textNodes + if type(node.static_text) == 'table' then res[#res + 1] = table.concat(node.static_text, '\n') end + end + + -- Fallback + if #res == 1 then + -- Prefer docTrig over trigger + ---@diagnostic disable-next-line: undefined-field + if snippet.docTrig then return snippet.docTrig end + return snippet.trigger + end + + return table.concat(res, '') +end + ---@param opts blink.cmp.LuasnipSourceOptions function source.new(opts) local self = setmetatable({}, { __index = source }) @@ -116,7 +138,7 @@ function source:get_completions(ctx, callback) local item = { kind = kind_snippet, label = snip.regTrig and snip.name or snip.trigger, - insertText = self.opts.prefer_doc_trig and snip.docTrig or snip.trigger, + insertText = get_insert_text(snip), insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText, sortText = sort_text, data = { snip_id = snip.id, show_condition = snip.show_condition }, From 1888b7b56678187c4a39a04ba0917492c1de7aa9 Mon Sep 17 00:00:00 2001 From: Soifou Date: Mon, 3 Nov 2025 21:12:38 +0100 Subject: [PATCH 09/13] chore(luasnip): deprecate `opts.prefer_doc_trig` This setting, which prefers `docTrig` over the trigger, is confusing and has seen little adoption despite its usefulness. Users can still opt out in v1.0, but the option will be removed in v2.0 to simplify the codebase. --- doc/configuration/reference.md | 2 +- lua/blink/cmp/sources/snippets/luasnip.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/configuration/reference.md b/doc/configuration/reference.md index a2bb31ce..02b944dc 100644 --- a/doc/configuration/reference.md +++ b/doc/configuration/reference.md @@ -581,7 +581,7 @@ sources.providers = { -- Whether to show autosnippets in the completion list show_autosnippets = true, -- Whether to prefer docTrig placeholders over trig when expanding regTrig snippets - prefer_doc_trig = false, + prefer_doc_trig = true, -- Whether to put the snippet description in the label description use_label_description = false, } diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index d8434022..09a1cb9d 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -7,7 +7,7 @@ local kind_snippet = require('blink.cmp.types').CompletionItemKind.Snippet --- @class blink.cmp.LuasnipSourceOptions --- @field use_show_condition? boolean Whether to use show_condition for filtering snippets --- @field show_autosnippets? boolean Whether to show autosnippets in the completion list ---- @field prefer_doc_trig? boolean When expanding `regTrig` snippets, prefer `docTrig` over `trig` placeholder +--- @field prefer_doc_trig? boolean When expanding `regTrig` snippets, prefer `docTrig` over `trig` placeholder (deprecated) --- @field use_label_description? boolean Whether to put the snippet description in the label description --- @class blink.cmp.LuasnipSource : blink.cmp.Source @@ -55,13 +55,13 @@ function source.new(opts) opts = vim.tbl_deep_extend('keep', opts or {}, { use_show_condition = true, show_autosnippets = true, - prefer_doc_trig = false, + prefer_doc_trig = true, -- TODO: Remove in v2.0 use_label_description = false, }) require('blink.cmp.config.utils').validate('sources.providers.snippets.opts', { use_show_condition = { opts.use_show_condition, 'boolean' }, show_autosnippets = { opts.show_autosnippets, 'boolean' }, - prefer_doc_trig = { opts.prefer_doc_trig, 'boolean' }, + prefer_doc_trig = { opts.prefer_doc_trig, 'boolean' }, -- TODO: Remove in v2.0 use_label_description = { opts.use_label_description, 'boolean' }, }, opts) From f082d1ca81d8faaa040fc913fe0c68c80a4d571a Mon Sep 17 00:00:00 2001 From: Soifou Date: Tue, 11 Nov 2025 19:50:15 +0100 Subject: [PATCH 10/13] refactor(luasnip): wip Update lua annotations using refactor branch Fix `docTrig` replacement using Luasnip v2.4.1 Add initial support for choice nodes --- lua/blink/cmp/sources/snippets/luasnip.lua | 105 ++++++++++++++------- 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index 09a1cb9d..f12a3ea9 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -1,5 +1,6 @@ ---@type LuaSnip.API local luasnip +local cmp = require('blink.cmp') local utils = require('blink.cmp.lib.utils') local text_edits = require('blink.cmp.lib.text_edits') local kind_snippet = require('blink.cmp.types').CompletionItemKind.Snippet @@ -26,21 +27,55 @@ local function add_luasnip_callback(snippet, event, callback) snippet.callbacks[-1][events[event]] = callback end +---@param snippet LuaSnip.Snippet +local function regex_callback(snippet, docTrig) + if #snippet.insert_nodes == 0 then + snippet.insert_nodes[0].static_text[1] = docTrig + return + end + + local matches = { string.match(docTrig, snippet.trigger) } + for i, match in ipairs(matches) do + local idx = i ~= #matches and i or 0 + snippet.insert_nodes[idx].static_text[1] = match + end +end + +---@param snippet LuaSnip.Snippet +local function choice_callback(snippet) + local events = require('luasnip.util.events') + local types = require('luasnip.util.types') + + for _, node in ipairs(snippet.insert_nodes) do + if node.type == types.choiceNode then + node.node_callbacks = { + [events.enter] = function( + n --[[@cast n LuaSnip.ChoiceNode]] + ) + vim.schedule(function() + local index = utils.find_idx(n.choices, function(choice) return choice == n.active_choice end) + n:set_text_raw({ '' }) -- NOTE: Available since v2.4.1 + cmp.show({ initial_selected_item_idx = index, providers = { 'snippets' } }) + end) + end, + [events.change_choice] = function() + vim.schedule(function() luasnip.jump(1) end) + end, + [events.leave] = function() vim.schedule(cmp.hide) end, + } + end + end +end + ---@param snippet LuaSnip.Snippet ---@return string? local function get_insert_text(snippet) local res = {} for _, node in ipairs(snippet.nodes) do - ---@cast node LuaSnip.Node - -- TODO: How to know the node type? Would be nice to handle the others as well - -- textNodes - if type(node.static_text) == 'table' then res[#res + 1] = table.concat(node.static_text, '\n') end + if node.static_text then res[#res + 1] = table.concat(node:get_static_text(), '\n') end end - -- Fallback if #res == 1 then - -- Prefer docTrig over trigger - ---@diagnostic disable-next-line: undefined-field if snippet.docTrig then return snippet.docTrig end return snippet.trigger end @@ -97,7 +132,24 @@ function source:get_completions(ctx, callback) --- @type blink.cmp.CompletionItem[] local items = {} - -- Gather snippets from relevant filetypes, including extensions + if luasnip.choice_active() then + ---@type LuaSnip.ChoiceNode + local active_choice = require('luasnip.session').active_choice_nodes[ctx.bufnr] + for i, choice in ipairs(active_choice.choices) do + local text = choice:get_static_text()[1] + table.insert(items, { + label = text, + kind = kind_snippet, + insertText = text, + insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText, + data = { snip_id = active_choice.parent.snippet.id, choice_index = i }, + }) + end + callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = items }) + return + end + + -- Else, gather snippets from relevant filetypes, including extensions for _, ft in ipairs(require('luasnip.util.util').get_snippet_filetypes()) do if self.items_cache[ft] and #self.items_cache[ft] > 0 then for _, item in ipairs(self.items_cache[ft]) do @@ -114,7 +166,7 @@ function source:get_completions(ctx, callback) if self.opts.show_autosnippets then local autosnippets = luasnip.get_snippets(ft, { type = 'autosnippets' }) for _, s in ipairs(autosnippets) do - add_luasnip_callback(s, 'enter', require('blink.cmp').hide) + add_luasnip_callback(s, 'enter', cmp.hide) end snippets = utils.shallow_copy(snippets) vim.list_extend(snippets, autosnippets) @@ -180,11 +232,9 @@ function source:resolve(item, callback) if type(detail) == 'table' then detail = table.concat(detail, '\n') end resolved_item.detail = detail - ---@diagnostic disable-next-line: undefined-field if snip.dscr then resolved_item.documentation = { kind = 'markdown', - ---@diagnostic disable-next-line: undefined-field value = table.concat(vim.lsp.util.convert_input_to_markdown_lines(snip.dscr), '\n'), } end @@ -195,34 +245,26 @@ end ---@param ctx blink.cmp.Context ---@param item blink.cmp.CompletionItem function source:execute(ctx, item) + if item.data.choice_index then + luasnip.set_choice(item.data.choice_index) + return + end + local snip = luasnip.get_id_snippet(item.data.snip_id) - -- if trigger is a pattern, expand "pattern" instead of actual snippet - ---@diagnostic disable-next-line: undefined-field if snip.regTrig then - ---@diagnostic disable-next-line: undefined-field local docTrig = self.opts.prefer_doc_trig and snip.docTrig - snip = snip:get_pattern_expand_helper() --[[@as LuaSnip.Snippet]] - - if docTrig then - add_luasnip_callback(snip, 'pre_expand', function(snip, _) - if #snip.insert_nodes == 0 then - snip.insert_nodes[0].static_text = { docTrig } - else - local matches = { string.match(docTrig, snip.trigger) } - for i, match in ipairs(matches) do - local idx = i ~= #matches and i or 0 - snip.insert_nodes[idx].static_text = { match } - end - end - end) - end + snip = snip:get_pattern_expand_helper() + if docTrig then add_luasnip_callback(snip, 'pre_expand', function(s) regex_callback(s, docTrig) end) end + else + add_luasnip_callback(snip, 'pre_expand', choice_callback) end local cursor = ctx.get_cursor() --[[@as LuaSnip.BytecolBufferPosition]] cursor[1] = cursor[1] - 1 local range = text_edits.get_from_item(item).range + ---@type LuaSnip.BufferRegion local clear_region = { from = { range.start.line, range.start.character }, @@ -233,15 +275,12 @@ function source:execute(ctx, item) local line_to_cursor = line:sub(1, cursor[2]) local range_text = line:sub(range.start.character + 1, cursor[2]) - ---@type LuaSnip.Opts.SnipExpandExpandParams? local expand_params = snip:matches(line_to_cursor, { - fallback_match = range_text ~= line_to_cursor and range_text, + fallback_match = range_text ~= line_to_cursor and range_text or nil, }) if expand_params ~= nil then - ---@diagnostic disable-next-line: undefined-field if expand_params.clear_region ~= nil then - ---@diagnostic disable-next-line: undefined-field clear_region = expand_params.clear_region elseif expand_params.trigger ~= nil then clear_region.from = { cursor[1], cursor[2] - #expand_params.trigger } From a2806244bc2261c7610bfeb9712ab36617d25284 Mon Sep 17 00:00:00 2001 From: Soifou Date: Tue, 11 Nov 2025 23:50:44 +0100 Subject: [PATCH 11/13] perf(luasnip): reduce table lookups, again --- lua/blink/cmp/sources/snippets/luasnip.lua | 61 ++++++++++++---------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index f12a3ea9..1c9d8a9f 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -14,17 +14,17 @@ local kind_snippet = require('blink.cmp.types').CompletionItemKind.Snippet --- @class blink.cmp.LuasnipSource : blink.cmp.Source --- @field opts blink.cmp.LuasnipSourceOptions --- @field items_cache table +--- @field has_loaded boolean local source = {} ---@param snippet table ----@param event string +---@param event number ---@param callback fun(table, table) local function add_luasnip_callback(snippet, event, callback) - local events = require('luasnip.util.events') -- not defined for autosnippets if snippet.callbacks == nil then return end snippet.callbacks[-1] = snippet.callbacks[-1] or {} - snippet.callbacks[-1][events[event]] = callback + snippet.callbacks[-1][event] = callback end ---@param snippet LuaSnip.Snippet @@ -42,8 +42,7 @@ local function regex_callback(snippet, docTrig) end ---@param snippet LuaSnip.Snippet -local function choice_callback(snippet) - local events = require('luasnip.util.events') +local function choice_callback(snippet, events) local types = require('luasnip.util.types') for _, node in ipairs(snippet.insert_nodes) do @@ -102,29 +101,32 @@ function source.new(opts) self.opts = opts self.items_cache = {} + self.has_loaded = false - local luasnip_ag = vim.api.nvim_create_augroup('BlinkCmpLuaSnipReload', { clear = true }) - vim.api.nvim_create_autocmd('User', { - pattern = 'LuasnipSnippetsAdded', - callback = function() self:reload() end, - group = luasnip_ag, - desc = 'Reset internal cache of luasnip source of blink.cmp when new snippets are added', - }) - vim.api.nvim_create_autocmd('User', { - pattern = 'LuasnipCleanup', - callback = function() self:reload() end, - group = luasnip_ag, - desc = 'Reload luasnip source of blink.cmp when snippets are cleared', - }) + local ok, mod = pcall(require, 'luasnip') + if ok then + self.has_loaded = true + luasnip = mod + + local luasnip_ag = vim.api.nvim_create_augroup('BlinkCmpLuaSnipReload', { clear = true }) + local events = { + { pattern = 'LuasnipSnippetsAdded', desc = 'Clear the Luasnip cache in blink.cmp when new snippets are added' }, + { pattern = 'LuasnipCleanup', desc = 'Clear the Luasnip cache in blink.cmp when snippets are cleared' }, + } + for _, event in ipairs(events) do + vim.api.nvim_create_autocmd('User', { + pattern = event.pattern, + callback = function() self:reload() end, + group = luasnip_ag, + desc = event.desc, + }) + end + end return self end -function source:enabled() - local ok, mod = pcall(require, 'luasnip') - if ok then luasnip = mod end - return ok -end +function source:enabled() return self.has_loaded end ---@param ctx blink.cmp.Context ---@param callback fun(result?: blink.cmp.CompletionResponse) @@ -134,7 +136,7 @@ function source:get_completions(ctx, callback) if luasnip.choice_active() then ---@type LuaSnip.ChoiceNode - local active_choice = require('luasnip.session').active_choice_nodes[ctx.bufnr] + local active_choice = luasnip.session.active_choice_nodes[ctx.bufnr] for i, choice in ipairs(active_choice.choices) do local text = choice:get_static_text()[1] table.insert(items, { @@ -149,8 +151,10 @@ function source:get_completions(ctx, callback) return end + local events = require('luasnip.util.events') + -- Else, gather snippets from relevant filetypes, including extensions - for _, ft in ipairs(require('luasnip.util.util').get_snippet_filetypes()) do + for _, ft in ipairs(luasnip.get_snippet_filetypes()) do if self.items_cache[ft] and #self.items_cache[ft] > 0 then for _, item in ipairs(self.items_cache[ft]) do table.insert(items, utils.shallow_copy(item)) @@ -166,7 +170,7 @@ function source:get_completions(ctx, callback) if self.opts.show_autosnippets then local autosnippets = luasnip.get_snippets(ft, { type = 'autosnippets' }) for _, s in ipairs(autosnippets) do - add_luasnip_callback(s, 'enter', cmp.hide) + add_luasnip_callback(s, events.enter, cmp.hide) end snippets = utils.shallow_copy(snippets) vim.list_extend(snippets, autosnippets) @@ -252,12 +256,13 @@ function source:execute(ctx, item) local snip = luasnip.get_id_snippet(item.data.snip_id) + local events = require('luasnip.util.events') if snip.regTrig then local docTrig = self.opts.prefer_doc_trig and snip.docTrig snip = snip:get_pattern_expand_helper() - if docTrig then add_luasnip_callback(snip, 'pre_expand', function(s) regex_callback(s, docTrig) end) end + if docTrig then add_luasnip_callback(snip, events.pre_expand, function(s) regex_callback(s, docTrig) end) end else - add_luasnip_callback(snip, 'pre_expand', choice_callback) + add_luasnip_callback(snip, events.pre_expand, function(s) choice_callback(s, events) end) end local cursor = ctx.get_cursor() --[[@as LuaSnip.BytecolBufferPosition]] From 8d79254104021c6c7f8e2910961122fc73f9536a Mon Sep 17 00:00:00 2001 From: Soifou Date: Thu, 13 Nov 2025 12:29:47 +0100 Subject: [PATCH 12/13] feat(luasnip): improve inserted text --- lua/blink/cmp/sources/snippets/luasnip.lua | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index 1c9d8a9f..ee58b030 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -69,17 +69,19 @@ end ---@param snippet LuaSnip.Snippet ---@return string? local function get_insert_text(snippet) + if snippet.docTrig then return snippet.docTrig end + + local types = require('luasnip.util.types') local res = {} for _, node in ipairs(snippet.nodes) do - if node.static_text then res[#res + 1] = table.concat(node:get_static_text(), '\n') end - end - -- Fallback - if #res == 1 then - if snippet.docTrig then return snippet.docTrig end - return snippet.trigger + if node.static_text then + res[#res + 1] = table.concat(node:get_static_text(), '\n') + elseif vim.tbl_contains({ types.dynamicNode, types.functionNode }, node.type) then + res[#res + 1] = 'xxxxxxx' + end end - return table.concat(res, '') + return #res == 1 and snippet.trigger or table.concat(res, '') end ---@param opts blink.cmp.LuasnipSourceOptions From 8b180f0a5fd755a02dc9ff784b6e8d97341eeac9 Mon Sep 17 00:00:00 2001 From: Soifou Date: Thu, 13 Nov 2025 12:30:31 +0100 Subject: [PATCH 13/13] fix(luasnip): restore choice text on leave --- lua/blink/cmp/sources/snippets/luasnip.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lua/blink/cmp/sources/snippets/luasnip.lua b/lua/blink/cmp/sources/snippets/luasnip.lua index ee58b030..21cc4aa9 100644 --- a/lua/blink/cmp/sources/snippets/luasnip.lua +++ b/lua/blink/cmp/sources/snippets/luasnip.lua @@ -48,19 +48,19 @@ local function choice_callback(snippet, events) for _, node in ipairs(snippet.insert_nodes) do if node.type == types.choiceNode then node.node_callbacks = { - [events.enter] = function( - n --[[@cast n LuaSnip.ChoiceNode]] - ) - vim.schedule(function() - local index = utils.find_idx(n.choices, function(choice) return choice == n.active_choice end) - n:set_text_raw({ '' }) -- NOTE: Available since v2.4.1 - cmp.show({ initial_selected_item_idx = index, providers = { 'snippets' } }) - end) + [events.enter] = function(n) + --[[@cast n LuaSnip.ChoiceNode]] + n:set_text({ '' }) -- NOTE: Clear the current text, we'll restore it when leaving the node. Available since v2.4.1 + local index = utils.find_idx(n.choices, function(choice) return choice == n.active_choice end) + vim.schedule(function() cmp.show({ initial_selected_item_idx = index, providers = { 'snippets' } }) end) end, [events.change_choice] = function() vim.schedule(function() luasnip.jump(1) end) end, - [events.leave] = function() vim.schedule(cmp.hide) end, + [events.leave] = function(n) + --[[@cast n LuaSnip.ChoiceNode]] + n:set_text(n.active_choice.static_text) + end, } end end