summaryrefslogtreecommitdiff
path: root/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/luasnip.lua
blob: 91716d255f9f09b92998dd1bca06d4253852fd0b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
--- @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

--- @class blink.cmp.LuasnipSource : blink.cmp.Source
--- @field config blink.cmp.LuasnipSourceOptions
--- @field items_cache table<string, blink.cmp.CompletionItem[]>

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

local defaults_config = {
  use_show_condition = true,
  show_autosnippets = true,
}

function source.new(opts)
  local config = vim.tbl_deep_extend('keep', opts, defaults_config)
  require('blink.cmp.config.utils').validate('sources.providers.luasnip', {
    use_show_condition = { config.use_show_condition, 'boolean' },
    show_autosnippets = { config.show_autosnippets, 'boolean' },
  }, config)

  local self = setmetatable({}, { __index = source })
  self.config = config
  self.items_cache = {}

  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',
  })

  return self
end

function source:enabled()
  local ok, _ = pcall(require, 'luasnip')
  return ok
end

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
      vim.list_extend(items, self.items_cache[ft])
      goto continue
    end

    -- 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' })
    if self.config.show_autosnippets then
      local autosnippets = require('luasnip').get_snippets(ft, { type = 'autosnippets' })
      snippets = require('blink.cmp.lib.utils').shallow_copy(snippets)
      vim.list_extend(snippets, autosnippets)
    end
    snippets = vim.tbl_filter(function(snip) return not snip.hidden end, snippets)

    -- Get the max priority for use with sortText
    local max_priority = 0
    for _, snip in ipairs(snippets) do
      max_priority = math.max(max_priority, snip.effective_priority or 0)
    end

    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
      local inversed_priority = max_priority - (snip.effective_priority or 0)
      local sort_text = ('0'):rep(8 - #tostring(inversed_priority), '') .. inversed_priority .. '|||' .. snip.trigger

      --- @type lsp.CompletionItem
      local item = {
        kind = require('blink.cmp.types').CompletionItemKind.Snippet,
        label = snip.trigger,
        insertText = snip.trigger,
        insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText,
        sortText = sort_text,
        data = { snip_id = snip.id, show_condition = snip.show_condition },
      }
      -- 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
      table.insert(items, item)
    end

    ::continue::
  end

  -- Filter items based on show_condition, if configured
  if self.config.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

  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 resolved_item = vim.deepcopy(item)

  local detail = snip:get_docstring()
  if type(detail) == 'table' then detail = table.concat(detail, '\n') end
  resolved_item.detail = detail

  if snip.dscr then
    resolved_item.documentation = {
      kind = 'markdown',
      value = table.concat(vim.lsp.util.convert_input_to_markdown_lines(snip.dscr), '\n'),
    }
  end

  callback(resolved_item)
end

function source:execute(_, 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
  if snip.regTrig then snip = snip:get_pattern_expand_helper() end

  -- get (0, 0) indexed cursor position
  -- the completion has been accepted by this point, so ctx.cursor is out of date
  local cursor = vim.api.nvim_win_get_cursor(0)
  cursor[1] = cursor[1] - 1

  local expand_params = snip:matches(require('luasnip.util.util').get_current_line_to_cursor())

  local clear_region = {
    from = { cursor[1], cursor[2] - #item.insertText },
    to = cursor,
  }
  if expand_params ~= nil and expand_params.clear_region ~= nil then
    clear_region = expand_params.clear_region
  elseif expand_params ~= nil and expand_params.trigger ~= nil then
    clear_region = {
      from = { cursor[1], cursor[2] - #expand_params.trigger },
      to = cursor,
    }
  end

  luasnip.snip_expand(snip, { expand_params = expand_params, clear_region = clear_region })
end

function source:reload() self.items_cache = {} end

return source