Skip to content

Commit

Permalink
feat: native luasnip source
Browse files Browse the repository at this point in the history
Thanks to @leiserfg and @soifou!
Closes #378
Closes #401
Closes #432

Co-authored-by: leiserfg <[email protected]>
Co-authored-by: soifou <[email protected]>
  • Loading branch information
3 people committed Dec 3, 2024
1 parent f4e53f2 commit 08b59ed
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 26 deletions.
28 changes: 3 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,17 +699,11 @@ MiniDeps.add({

### Luasnip

There's currently no `blink.cmp` native source for [luasnip](https://github.com/L3MON4D3/LuaSnip). You may use [blink.compat](https://github.com/saghen/blink.compat) plugin with the [cmp_luasnip](https://github.com/saadparwaiz1/cmp_luasnip) nvim-cmp source in the meantime.

```lua
{
'saghen/blink.cmp',
version = '0.*',
dependencies = {
'L3MON4D3/LuaSnip',
'saadparwaiz1/cmp_luasnip',
-- lock compat to tagged versions, if you've also locked blink.cmp to tagged versions
{ 'saghen/blink.compat', version = '*', opts = { impersonate_nvim_cmp = true } } },
version = 'v0.*',
dependencies = 'L3MON4D3/LuaSnip',
opts = {
snippets = {
expand = function(snippet) require('luasnip').lsp_expand(snippet) end,
Expand All @@ -723,23 +717,7 @@ There's currently no `blink.cmp` native source for [luasnip](https://github.com/
},
sources = {
completion = {
-- WARN: add the rest of your providers here, unless you're using `opts_extend`
-- and defining this outside of your primary `blink.cmp` config
-- see the default configuration for the default providers
enabled_providers = { 'luasnip' },
},
providers = {
luasnip = {
name = 'luasnip',
module = 'blink.compat.source',

score_offset = -3,

opts = {
use_show_condition = false,
show_autosnippets = true,
},
},
enabled_providers = { 'lsp', 'path', 'luasnip', 'buffer' },
},
},
}
Expand Down
5 changes: 5 additions & 0 deletions lua/blink/cmp/config/sources.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ local sources = {
module = 'blink.cmp.sources.snippets',
score_offset = -3,
},
luasnip = {
name = 'Luasnip',
module = 'blink.cmp.sources.luasnip',
score_offset = -3,
},
buffer = {
name = 'Buffer',
module = 'blink.cmp.sources.buffer',
Expand Down
2 changes: 1 addition & 1 deletion lua/blink/cmp/sources/lib/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
--- @field items blink.cmp.CompletionItem[]

--- @class blink.cmp.Source
--- @field new fun(config: blink.cmp.SourceProviderConfig): blink.cmp.Source
--- @field new fun(opts: table, config: blink.cmp.SourceProviderConfig): blink.cmp.Source
--- @field enabled? fun(self: blink.cmp.Source, context: blink.cmp.Context): boolean
--- @field get_trigger_characters? fun(self: blink.cmp.Source): string[]
--- @field get_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, callback: fun(response?: blink.cmp.CompletionResponse)): (fun(): nil) | nil
Expand Down
138 changes: 138 additions & 0 deletions lua/blink/cmp/sources/luasnip.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---@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 or {}, defaults_config)
vim.validate({
use_show_condition = { config.use_show_condition, 'boolean' },
show_autosnippets = { config.show_autosnippets, 'boolean' },
})
local self = setmetatable({}, { __index = source })
self.config = config
self.items_cache = {}
return self
end

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

function source:get_completions(ctx, callback)
local ft = vim.bo.filetype

if not self.items_cache[ft] then
--- @type blink.cmp.CompletionItem[]
local items = {}

-- 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
if not snip.hidden then max_priority = math.max(max_priority, snip.effective_priority or 0) end
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 },
}
table.insert(items, item)
end

self.items_cache[ft] = items
end

local items = self.items_cache[ft] or {}

-- 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)
resolved_item.detail = snip:get_docstring()
resolved_item.documentation = {
kind = 'markdown',
value = table.concat(vim.lsp.util.convert_input_to_markdown_lines(item.data.documentation or ''), '\n'),
}

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
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

0 comments on commit 08b59ed

Please sign in to comment.