Skip to content

Commit

Permalink
Merge pull request #3 from abeldekat/add_caching
Browse files Browse the repository at this point in the history
Add caching for completion items. Enabled by default
Reuse completion_item.documentation if it was already generated
  • Loading branch information
abeldekat authored Jan 5, 2025
2 parents 8bb622d + 66975c7 commit ee89749
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 47 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ return {

</details>

## Options

The default options can be modified in the `sources` field of the `nvim-cmp` spec.

```lua
sources = cmp.config.sources({
{
name = "mini_snippets",
option = {
-- completion items are cached using default mini.snippets context:
use_items_cache = false -- default: true
}
}
}),
```

## LazyVim and mini.snippets

See this [LazyVim] PR for a mini.snippets "extra"...
Expand Down
145 changes: 98 additions & 47 deletions lua/cmp_mini_snippets/init.lua
Original file line number Diff line number Diff line change
@@ -1,90 +1,141 @@
-- :h cmp-develop

---@class cmp_mini_snippets.Options
--- @field use_items_cache? boolean completion items are cached using default mini.snippets context

---@type cmp_mini_snippets.Options
local defaults = {
use_items_cache = true, -- allow the user to disable caching completion items
}

local cmp = require("cmp")
local util = require("vim.lsp.util")

local source = {}

-- Creates a markdown representation of the snippet
-- A fenced code block after convert_input_to_markdown_lines is probably ok.
---@return string
local function get_documentation(snippet)
local header = (snippet.prefix or "") .. " _ `[" .. vim.bo.filetype .. "]`\n"
local docstring = { "", "```" .. vim.bo.filetype, snippet.body, "```" }
local documentation = { header .. "---", (snippet.desc or ""), docstring }
documentation = util.convert_input_to_markdown_lines(documentation)

return table.concat(documentation, "\n")
source.new = function()
local self = setmetatable({}, { __index = source })
self.items_cache = {}
return self
end

-- Remove the word inserted by nvim-cmp and insert snippet
-- It's safe to assume that mode is insert during completion
local function insert_snippet(snippet, word)
local cursor = vim.api.nvim_win_get_cursor(0)
cursor[1] = cursor[1] - 1 -- nvim_buf_set_text: line is zero based
local start_col = cursor[2] - #word
vim.api.nvim_buf_set_text(0, cursor[1], start_col, cursor[1], cursor[2], {})

local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert
insert({ body = snippet.body }) -- insert at cursor
---@return cmp_mini_snippets.Options
local function get_valid_options(params)
local opts = vim.tbl_deep_extend("keep", params.option, defaults)
vim.validate({
use_items_cache = { opts.use_items_cache, "boolean" },
})
return opts
end

source.new = function() return setmetatable({}, { __index = source }) end

---Return the keyword pattern for triggering completion (optional).
---If this is omitted, nvim-cmp will use a default keyword pattern. See |cmp-config.completion.keyword_pattern|.
---Using the same keyword pattern as cmp-luasnip
---@return string
source.get_keyword_pattern = function() -- same as cmp-luasnip!
return "\\%([^[:alnum:][:blank:]]\\|\\w\\+\\)"
end

-- Copied from :h nvim-cmp as a reminder...
---Return trigger characters for triggering completion (optional).
-- function source:get_trigger_characters() return { "." } end
source.get_keyword_pattern = function() return "\\%([^[:alnum:][:blank:]]\\|\\w\\+\\)" end

---Return whether this source is available in the current context or not (optional).
---@return boolean
function source:is_available()
---@diagnostic disable-next-line: undefined-field
return _G.MiniSnippets ~= nil -- ensure that user has explicitly setup mini.snippets
end

---Invoke completion (required).
-- -@param params cmp.SourceCompletionApiParams
---@param callback fun(response: lsp.CompletionResponse|nil)
function source:complete(_, callback)
local items = {}
local snippets = MiniSnippets.expand({ match = false, insert = false })
if not snippets then return items end
local function to_completion_items(snippets)
local result = {}

for _, snippet in ipairs(snippets) do
items[#items + 1] = {
word = snippet.prefix,
label = snippet.prefix,
for _, snip in ipairs(snippets) do
local item = {
word = snip.prefix,
label = snip.prefix,
kind = cmp.lsp.CompletionItemKind.Snippet,
data = { snippet = snippet }, -- cmp-luasnip only stores the snippet-id...
data = { snip = snip },
}
table.insert(result, item)
end
return result
end

-- NOTE: Completion items are cached by default using the default 'mini.snippets' context
--
-- vim.b.minisnippets_config can contain buffer-local snippets.
-- a buffer can contain code in multiple languages
--
-- See :h MiniSnippets.default_prepare
--
-- Return completion items produced from snippets either directly or from cache
local function get_completion_items(cache)
if not cache then return to_completion_items(MiniSnippets.expand({ match = false, insert = false })) end

-- Compute cache id
local _, context = MiniSnippets.default_prepare({})
local id = "buf=" .. context.buf_id .. ",lang=" .. context.lang

-- Return the completion items for this context from cache
if cache[id] then return cache[id] end

-- Retrieve all raw snippets in context and transform into completion items
local snippets = MiniSnippets.expand({ match = false, insert = false })
local items = to_completion_items(vim.deepcopy(snippets))
cache[id] = items

return items
end

---Invoke completion (required).
-- @param params cmp.SourceCompletionApiParams
---@param callback fun(response: lsp.CompletionResponse|nil)
function source:complete(params, callback)
local opts = get_valid_options(params)
local cache = opts.use_items_cache and self.items_cache or nil
local items = get_completion_items(cache)
callback(items)
end

-- Creates a markdown representation of the snippet
-- A fenced code block after convert_input_to_markdown_lines is probably ok.
---@return string
local function get_documentation(snip)
local header = (snip.prefix or "") .. " _ `[" .. vim.bo.filetype .. "]`\n"
local docstring = { "", "```" .. vim.bo.filetype, snip.body, "```" }
local documentation = { header .. "---", (snip.desc or ""), docstring }
documentation = util.convert_input_to_markdown_lines(documentation)

return table.concat(documentation, "\n")
end

---Resolve completion item (optional). This is called right before the completion is about to be displayed.
---Useful for setting the text shown in the documentation window (`completion_item.documentation`).
---@param completion_item lsp.CompletionItem
---@param callback fun(completion_item: lsp.CompletionItem|nil)
function source:resolve(completion_item, callback) -- modified from cmp-luasnip:
local snippet = completion_item.data.snippet
completion_item.documentation = {
kind = cmp.lsp.MarkupKind.Markdown,
value = get_documentation(snippet),
}
if not completion_item.documentation then
completion_item.documentation = {
kind = cmp.lsp.MarkupKind.Markdown,
value = get_documentation(completion_item.data.snip),
}
end

callback(completion_item)
end

-- Remove the word inserted by nvim-cmp and insert snippet
-- It's safe to assume that mode is insert during completion
local function insert_snippet(snip, word)
local cursor = vim.api.nvim_win_get_cursor(0)
cursor[1] = cursor[1] - 1 -- nvim_buf_set_text: line is zero based
local start_col = cursor[2] - #word
vim.api.nvim_buf_set_text(0, cursor[1], start_col, cursor[1], cursor[2], {})

local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert
insert({ body = snip.body }) -- insert at cursor
end

---Executed after the item was selected.
---@param completion_item lsp.CompletionItem
---@param callback fun(completion_item: lsp.CompletionItem|nil)
function source:execute(completion_item, callback)
insert_snippet(completion_item.data.snippet, completion_item.word)
insert_snippet(completion_item.data.snip, completion_item.word)
callback(completion_item)
end

Expand Down

0 comments on commit ee89749

Please sign in to comment.