diff --git a/README.md b/README.md index 43480f7..894c088 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,22 @@ return { +## 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"... diff --git a/lua/cmp_mini_snippets/init.lua b/lua/cmp_mini_snippets/init.lua index 00559f2..87ae65e 100644 --- a/lua/cmp_mini_snippets/init.lua +++ b/lua/cmp_mini_snippets/init.lua @@ -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