---@class lazyvim.util.cmp local M = {} ---@alias lazyvim.util.cmp.Action fun():boolean? ---@type table M.actions = { -- Native Snippets snippet_forward = function() if vim.snippet.active({ direction = 1 }) then vim.schedule(function() vim.snippet.jump(1) end) return true end end, } ---@param actions string[] ---@param fallback? string|fun() function M.map(actions, fallback) return function() for _, name in ipairs(actions) do if M.actions[name] then local ret = M.actions[name]() if ret then return true end end end return type(fallback) == "function" and fallback() or fallback end end ---@alias Placeholder {n:number, text:string} ---@param snippet string ---@param fn fun(placeholder:Placeholder):string ---@return string function M.snippet_replace(snippet, fn) return snippet:gsub("%$%b{}", function(m) local n, name = m:match("^%${(%d+):(.+)}$") return n and fn({ n = n, text = name }) or m end) or snippet end -- This function resolves nested placeholders in a snippet. ---@param snippet string ---@return string function M.snippet_preview(snippet) local ok, parsed = pcall(function() return vim.lsp._snippet_grammar.parse(snippet) end) return ok and tostring(parsed) or M.snippet_replace(snippet, function(placeholder) return M.snippet_preview(placeholder.text) end):gsub("%$0", "") end -- This function replaces nested placeholders in a snippet with LSP placeholders. function M.snippet_fix(snippet) local texts = {} ---@type table return M.snippet_replace(snippet, function(placeholder) texts[placeholder.n] = texts[placeholder.n] or M.snippet_preview(placeholder.text) return "${" .. placeholder.n .. ":" .. texts[placeholder.n] .. "}" end) end ---@param entry cmp.Entry function M.auto_brackets(entry) local cmp = require("cmp") local Kind = cmp.lsp.CompletionItemKind local item = entry:get_completion_item() if vim.tbl_contains({ Kind.Function, Kind.Method }, item.kind) then local cursor = vim.api.nvim_win_get_cursor(0) local prev_char = vim.api.nvim_buf_get_text(0, cursor[1] - 1, cursor[2], cursor[1] - 1, cursor[2] + 1, {})[1] if prev_char ~= "(" and prev_char ~= ")" then local keys = vim.api.nvim_replace_termcodes("()", false, false, true) vim.api.nvim_feedkeys(keys, "i", true) end end end -- This function adds missing documentation to snippets. -- The documentation is a preview of the snippet. ---@param window cmp.CustomEntriesView|cmp.NativeEntriesView function M.add_missing_snippet_docs(window) local cmp = require("cmp") local Kind = cmp.lsp.CompletionItemKind local entries = window:get_entries() for _, entry in ipairs(entries) do if entry:get_kind() == Kind.Snippet then local item = entry:get_completion_item() if not item.documentation and item.insertText then item.documentation = { kind = cmp.lsp.MarkupKind.Markdown, value = string.format("```%s\n%s\n```", vim.bo.filetype, M.snippet_preview(item.insertText)), } end end end end function M.visible() ---@module 'blink.cmp' local blink = package.loaded["blink.cmp"] if blink then return blink.windows and blink.windows.autocomplete.win:is_open() end ---@module 'cmp' local cmp = package.loaded["cmp"] if cmp then return cmp.core.view:visible() end return false end -- This is a better implementation of `cmp.confirm`: -- * check if the completion menu is visible without waiting for running sources -- * create an undo point before confirming -- This function is both faster and more reliable. ---@param opts? {select: boolean, behavior: cmp.ConfirmBehavior} function M.confirm(opts) local cmp = require("cmp") opts = vim.tbl_extend("force", { select = true, behavior = cmp.ConfirmBehavior.Insert, }, opts or {}) return function(fallback) if cmp.core.view:visible() or vim.fn.pumvisible() == 1 then LazyVim.create_undo() if cmp.confirm(opts) then return end end return fallback() end end function M.expand(snippet) -- Native sessions don't support nested snippet sessions. -- Always use the top-level session. -- Otherwise, when on the first placeholder and selecting a new completion, -- the nested session will be used instead of the top-level session. -- See: https://github.com/LazyVim/LazyVim/issues/3199 local session = vim.snippet.active() and vim.snippet._session or nil local ok, err = pcall(vim.snippet.expand, snippet) if not ok then local fixed = M.snippet_fix(snippet) ok = pcall(vim.snippet.expand, fixed) local msg = ok and "Failed to parse snippet,\nbut was able to fix it automatically." or ("Failed to parse snippet.\n" .. err) LazyVim[ok and "warn" or "error"]( ([[%s ```%s %s ```]]):format(msg, vim.bo.filetype, snippet), { title = "vim.snippet" } ) end -- Restore top-level session when needed if session then vim.snippet._session = session end end ---@param opts cmp.ConfigSchema | {auto_brackets?: string[]} function M.setup(opts) for _, source in ipairs(opts.sources) do source.group_index = source.group_index or 1 end local parse = require("cmp.utils.snippet").parse require("cmp.utils.snippet").parse = function(input) local ok, ret = pcall(parse, input) if ok then return ret end return LazyVim.cmp.snippet_preview(input) end local cmp = require("cmp") cmp.setup(opts) cmp.event:on("confirm_done", function(event) if vim.tbl_contains(opts.auto_brackets or {}, vim.bo.filetype) then LazyVim.cmp.auto_brackets(event.entry) end end) cmp.event:on("menu_opened", function(event) LazyVim.cmp.add_missing_snippet_docs(event.window) end) end return M