Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/configuration/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
7 changes: 0 additions & 7 deletions lua/blink/cmp/sources/lib/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 "'
Expand Down
254 changes: 168 additions & 86 deletions lua/blink/cmp/sources/snippets/luasnip.lua
Original file line number Diff line number Diff line change
@@ -1,95 +1,180 @@
---@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

--- @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
--- @field config blink.cmp.LuasnipSourceOptions
--- @field opts blink.cmp.LuasnipSourceOptions
--- @field items_cache table<string, blink.cmp.CompletionItem[]>

local utils = require('blink.cmp.lib.utils')

--- @type blink.cmp.LuasnipSource
--- @diagnostic disable-next-line: missing-fields
--- @field has_loaded boolean
local source = {}

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 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
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, 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]]
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(n)
--[[@cast n LuaSnip.ChoiceNode]]
n:set_text(n.active_choice.static_text)
end,
}
end
end
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')
elseif vim.tbl_contains({ types.dynamicNode, types.functionNode }, node.type) then
res[#res + 1] = 'xxxxxxx'
end
end

return #res == 1 and snippet.trigger or table.concat(res, '')
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 = true, -- TODO: Remove in v2.0
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' }, -- TODO: Remove in v2.0
use_label_description = { opts.use_label_description, 'boolean' },
}, opts)

local self = setmetatable({}, { __index = source })
self.config = config
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, _ = pcall(require, 'luasnip')
return ok
end
function source:enabled() return self.has_loaded 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 = {}

-- 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 luasnip.choice_active() then
---@type LuaSnip.ChoiceNode
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, {
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

local events = require('luasnip.util.events')

-- Else, gather snippets from relevant filetypes, including extensions
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))
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 = require('luasnip').get_snippets(ft, { type = 'snippets' })
if self.config.show_autosnippets then
local autosnippets = require('luasnip').get_snippets(ft, { type = 'autosnippets' })
local snippets = luasnip.get_snippets(ft, { type = 'snippets' })
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, events.enter, 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)
Expand All @@ -100,6 +185,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
Expand All @@ -108,27 +194,29 @@ 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,
insertText = get_insert_text(snip),
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,
}
-- 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

-- 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
Expand All @@ -137,15 +225,15 @@ function source:get_completions(ctx, callback)
is_incomplete_forward = false,
is_incomplete_backward = false,
items = items,
context = ctx,
})
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)

---@type string|string[]|nil
local detail = snip:get_docstring()
if type(detail) == 'table' then detail = table.concat(detail, '\n') end
resolved_item.detail = detail
Expand All @@ -160,35 +248,31 @@ 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 luasnip = require('luasnip')
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
local events = require('luasnip.util.events')
if snip.regTrig then
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()

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
if docTrig then add_luasnip_callback(snip, events.pre_expand, function(s) regex_callback(s, docTrig) end) end
else
add_luasnip_callback(snip, events.pre_expand, function(s) choice_callback(s, events) 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 = require('blink.cmp.lib.text_edits').get_from_item(item).range
local range = text_edits.get_from_item(item).range

---@type LuaSnip.BufferRegion
local clear_region = {
from = { range.start.line, range.start.character },
to = cursor,
Expand All @@ -199,17 +283,15 @@ function source:execute(ctx, item)
local range_text = line:sub(range.start.character + 1, cursor[2])

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
if expand_params.clear_region ~= nil then
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

Expand Down