Skip to content
Draft
Prev Previous commit
Next Next commit
refactor(luasnip): wip
Update lua annotations using refactor branch
Fix `docTrig` replacement using Luasnip v2.4.1
Add initial support for choice nodes
  • Loading branch information
soifou committed Nov 11, 2025
commit f082d1ca81d8faaa040fc913fe0c68c80a4d571a
105 changes: 72 additions & 33 deletions lua/blink/cmp/sources/snippets/luasnip.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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 },
Expand All @@ -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 }
Expand Down