|
| 1 | +---@class blink.cmp.LuasnipSourceOptions |
| 2 | +---@field use_show_condition? boolean Whether to use show_condition for filtering snippets |
| 3 | +---@field show_autosnippets? boolean Whether to show autosnippets in the completion list |
| 4 | + |
| 5 | +--- @class blink.cmp.LuasnipSource : blink.cmp.Source |
| 6 | +--- @field config blink.cmp.LuasnipSourceOptions |
| 7 | +--- @field items_cache table<string, blink.cmp.CompletionItem[]> |
| 8 | + |
| 9 | +--- @type blink.cmp.LuasnipSource |
| 10 | +--- @diagnostic disable-next-line: missing-fields |
| 11 | +local source = {} |
| 12 | + |
| 13 | +local defaults_config = { |
| 14 | + use_show_condition = true, |
| 15 | + show_autosnippets = true, |
| 16 | +} |
| 17 | + |
| 18 | +function source.new(opts) |
| 19 | + local config = vim.tbl_deep_extend('keep', opts or {}, defaults_config) |
| 20 | + vim.validate({ |
| 21 | + use_show_condition = { config.use_show_condition, 'boolean' }, |
| 22 | + show_autosnippets = { config.show_autosnippets, 'boolean' }, |
| 23 | + }) |
| 24 | + local self = setmetatable({}, { __index = source }) |
| 25 | + self.config = config |
| 26 | + self.items_cache = {} |
| 27 | + return self |
| 28 | +end |
| 29 | + |
| 30 | +function source:enabled() |
| 31 | + local ok, _ = pcall(require, 'luasnip') |
| 32 | + return ok |
| 33 | +end |
| 34 | + |
| 35 | +function source:get_completions(ctx, callback) |
| 36 | + local ft = vim.bo.filetype |
| 37 | + |
| 38 | + if not self.items_cache[ft] then |
| 39 | + --- @type blink.cmp.CompletionItem[] |
| 40 | + local items = {} |
| 41 | + |
| 42 | + -- Gather filetype snippets and, optionally, autosnippets |
| 43 | + local snippets = require('luasnip').get_snippets(ft, { type = 'snippets' }) |
| 44 | + if self.config.show_autosnippets then |
| 45 | + local autosnippets = require('luasnip').get_snippets(ft, { type = 'autosnippets' }) |
| 46 | + snippets = require('blink.cmp.lib.utils').shallow_copy(snippets) |
| 47 | + vim.list_extend(snippets, autosnippets) |
| 48 | + end |
| 49 | + snippets = vim.tbl_filter(function(snip) return not snip.hidden end, snippets) |
| 50 | + |
| 51 | + -- Get the max priority for use with sortText |
| 52 | + local max_priority = 0 |
| 53 | + for _, snip in ipairs(snippets) do |
| 54 | + if not snip.hidden then max_priority = math.max(max_priority, snip.effective_priority or 0) end |
| 55 | + end |
| 56 | + |
| 57 | + for _, snip in ipairs(snippets) do |
| 58 | + -- Convert priority of 1000 (with max of 8000) to string like "00007000|||asd" for sorting |
| 59 | + -- This will put high priority snippets at the top of the list, and break ties based on the trigger |
| 60 | + local inversed_priority = max_priority - (snip.effective_priority or 0) |
| 61 | + local sort_text = ('0'):rep(8 - tostring(inversed_priority), '') .. inversed_priority .. '|||' .. snip.trigger |
| 62 | + |
| 63 | + --- @type lsp.CompletionItem |
| 64 | + local item = { |
| 65 | + kind = require('blink.cmp.types').CompletionItemKind.Snippet, |
| 66 | + label = snip.trigger, |
| 67 | + insertText = snip.trigger, |
| 68 | + insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText, |
| 69 | + sortText = sort_text, |
| 70 | + data = { snip_id = snip.id, show_condition = snip.show_condition }, |
| 71 | + } |
| 72 | + table.insert(items, item) |
| 73 | + end |
| 74 | + |
| 75 | + self.items_cache[ft] = items |
| 76 | + end |
| 77 | + |
| 78 | + local items = self.items_cache[ft] or {} |
| 79 | + |
| 80 | + -- Filter items based on show_condition, if configured |
| 81 | + if self.config.use_show_condition then |
| 82 | + local line_to_cursor = ctx.line:sub(0, ctx.cursor[2] - 1) |
| 83 | + items = vim.tbl_filter(function(item) return item.data.show_condition(line_to_cursor) end, items) |
| 84 | + end |
| 85 | + |
| 86 | + callback({ |
| 87 | + is_incomplete_forward = false, |
| 88 | + is_incomplete_backward = false, |
| 89 | + items = items, |
| 90 | + context = ctx, |
| 91 | + }) |
| 92 | +end |
| 93 | + |
| 94 | +function source:resolve(item, callback) |
| 95 | + local snip = require('luasnip').get_id_snippet(item.data.snip_id) |
| 96 | + |
| 97 | + local resolved_item = vim.deepcopy(item) |
| 98 | + resolved_item.detail = snip:get_docstring() |
| 99 | + resolved_item.documentation = { |
| 100 | + kind = 'markdown', |
| 101 | + value = table.concat(vim.lsp.util.convert_input_to_markdown_lines(item.data.documentation or ''), '\n'), |
| 102 | + } |
| 103 | + |
| 104 | + callback(resolved_item) |
| 105 | +end |
| 106 | + |
| 107 | +function source:execute(_, item) |
| 108 | + local luasnip = require('luasnip') |
| 109 | + local snip = luasnip.get_id_snippet(item.data.snip_id) |
| 110 | + |
| 111 | + -- if trigger is a pattern, expand "pattern" instead of actual snippet. |
| 112 | + if snip.regTrig then snip = snip:get_pattern_expand_helper() end |
| 113 | + |
| 114 | + -- get (0, 0) indexed cursor position |
| 115 | + local cursor = vim.api.nvim_win_get_cursor(0) |
| 116 | + cursor[1] = cursor[1] - 1 |
| 117 | + |
| 118 | + local expand_params = snip:matches(require('luasnip.util.util').get_current_line_to_cursor()) |
| 119 | + |
| 120 | + local clear_region = { |
| 121 | + from = { cursor[1], cursor[2] - #item.insertText }, |
| 122 | + to = cursor, |
| 123 | + } |
| 124 | + if expand_params ~= nil and expand_params.clear_region ~= nil then |
| 125 | + clear_region = expand_params.clear_region |
| 126 | + elseif expand_params ~= nil and expand_params.trigger ~= nil then |
| 127 | + clear_region = { |
| 128 | + from = { cursor[1], cursor[2] - #expand_params.trigger }, |
| 129 | + to = cursor, |
| 130 | + } |
| 131 | + end |
| 132 | + |
| 133 | + luasnip.snip_expand(snip, { expand_params = expand_params, clear_region = clear_region }) |
| 134 | +end |
| 135 | + |
| 136 | +function source:reload() self.items_cache = {} end |
| 137 | + |
| 138 | +return source |
0 commit comments