-- -- Completion configuration plugin -- ------------------------------------------------------------------------------------------------------------------------ -- Insert mode completion ------------------------------------------------------------------------------------------------------------------------ -- See `:help ins-completion-menu` vim.opt.autocomplete = true -- Show completion menu automatically vim.opt.completeopt = { "noselect", -- No item selected initially "fuzzy", "menuone", -- Show matches in a menu, even if there's only one match "popup", -- Menu items show extra info in the popup window } -- Completion sources (in order of priority) vim.opt.complete = { "o", -- 'omnifunc' } vim.opt.pumwidth = 25 vim.keymap.set("i", "", function() return vim.fn.pumvisible() == 1 and "" or "" end, { expr = true }) vim.keymap.set("i", "", function() return vim.fn.pumvisible() == 1 and "" or "" end, { expr = true }) -- Jump between snippet placeholders, falling back to native behavior when no snippet is active vim.keymap.set({ "i", "s" }, "", function() return vim.snippet.active({ direction = 1 }) and "lua vim.snippet.jump(1)" or "" end, { expr = true }) vim.keymap.set({ "i", "s" }, "", function() return vim.snippet.active({ direction = -1 }) and "lua vim.snippet.jump(-1)" or "" end, { expr = true }) ------------------------------------------------------------------------------------------------------------------------ -- Snippet expansion fallback ------------------------------------------------------------------------------------------------------------------------ -- Work around Neovim's snippet grammar rejecting placeholder defaults that mix text with nested tabstops (e.g. lua_ls's -- `${1:pairs(${2:t})}`): the `any_or_text` rule matches a single node, not a `(any + text)^1` sequence. On parse failure, -- strip the snippet markup and insert it as plain text instead of throwing. Remove once fixed upstream. -- The `@as table` cast retypes the module here so overriding `expand` isn't flagged as a duplicate field set. local snippet = vim.snippet --[[@as table]] local orig_expand = snippet.expand -- Reduce LSP snippet syntax to plain text: keep placeholder/variable defaults, the first choice, drop bare tabstops. local function strip_snippet(s) local prev repeat prev = s s = s:gsub("%$(%b{})", function(group) local body = group:sub(2, -2) local choice = body:match("^%d+|(.*)|$") if choice then return (choice:gsub(",.*$", "")) -- first choice end local default = body:match("^[%w_]+:(.*)$") if default then return default -- ${n:default} / ${VAR:default} end return "" -- bare ${n} / ${VAR} end) until s == prev s = s:gsub("%$[%w_]+", "") -- bare $n / $VAR s = s:gsub("\\([%$}{|,\\])", "%1") -- unescape return s end local function insert_plain(text) local win = vim.api.nvim_get_current_win() local row, col = unpack(vim.api.nvim_win_get_cursor(win)) local indent = vim.api.nvim_get_current_line():match("^%s*") or "" local lines = vim.split(text, "\n", { plain = true }) for i = 2, #lines do lines[i] = indent .. lines[i] end vim.api.nvim_buf_set_text(0, row - 1, col, row - 1, col, lines) local last = #lines local new_row = row - 1 + (last - 1) local new_col = last == 1 and col + #lines[1] or #lines[last] vim.api.nvim_win_set_cursor(win, { new_row + 1, new_col }) end snippet.expand = function(input) local ok = pcall(orig_expand, input) if not ok then insert_plain(strip_snippet(input)) vim.notify( "Snippet parse failed, inserted as plain text: " .. vim.inspect(input), vim.log.levels.WARN ) end end ------------------------------------------------------------------------------------------------------------------------ -- Snippet completion source ------------------------------------------------------------------------------------------------------------------------ -- Exposes `vim.b.snippets` (trigger word -> LSP snippet body) as a 'complete' function source. A filetype opts in from -- its ftplugin by setting `vim.b.snippets`, pointing 'completefunc' at this function, and appending "F" to 'complete'. -- Must be a global so `v:lua` can reach it; see `:help complete-functions` and `:help v:lua-call`. -- Render a snippet to preview text: resolve placeholders and their mirrors to the placeholder's default value. local function render_snippet(s) local defaults = {} for n, d in s:gmatch("%${(%d+):([^{}]*)}") do defaults[n] = d end local prev repeat prev = s s = s:gsub("%$(%b{})", function(group) local body = group:sub(2, -2) local choice = body:match("^%d+|(.*)|$") if choice then return (choice:gsub(",.*$", "")) -- first choice end local default = body:match("^[%w_]+:(.*)$") if default then return default -- ${n:default} / ${VAR:default} end return defaults[body] or "" -- ${n} / ${VAR} end) until s == prev s = s:gsub("%$(%d+)", function(n) return defaults[n] or "" -- $n mirror end) s = s:gsub("%$[%w_]+", "") -- bare $VAR s = s:gsub("\\([%$}{|,\\])", "%1") -- unescape return s end function _G.dotfiles_snippet_source(findstart, base) local snippets = vim.b.snippets or {} local line = vim.api.nvim_get_current_line() local col = vim.api.nvim_win_get_cursor(0)[2] if findstart == 1 then -- Walk back over trigger characters to find the range the match replaces. local start = col while start > 0 and line:sub(start, start):match("[%w_]") do start = start - 1 end return start end local items = {} for trigger, body in pairs(snippets) do if vim.startswith(trigger, base) then items[#items + 1] = { word = trigger, kind = "Snippet", menu = "[snippet]", info = render_snippet(body), user_data = { snippet = body }, } end end return items end -- Expand the snippet once one of the items above is accepted (its trigger word is inserted first, so remove it). vim.api.nvim_create_autocmd("CompleteDone", { desc = "Expand accepted snippet completion", group = vim.g.dotfiles.augroup, callback = function() local item = vim.v.completed_item local data = item.user_data if type(data) ~= "table" or not data.snippet then return end local row, col = unpack(vim.api.nvim_win_get_cursor(0)) vim.api.nvim_buf_set_text(0, row - 1, col - #item.word, row - 1, col, { "" }) vim.snippet.expand(data.snippet) end, }) ------------------------------------------------------------------------------------------------------------------------ -- Command-line mode completion ------------------------------------------------------------------------------------------------------------------------ -- See `:help cmdline-completion` and `:help cmdline-autocompletion` -- Show completion menu automatically vim.api.nvim_create_autocmd({ "CmdlineChanged", "CmdlineEnter" }, { desc = "Autocompletion", group = vim.g.dotfiles.augroup, pattern = "[:\\/\\?]", callback = function() vim.fn.wildtrigger() end, }) vim.opt.wildmenu = true -- Show completions in a menu vim.opt.wildchar = 9 -- Char code assigned to command line wildcard expansion vim.opt.pumborder = "rounded" vim.opt.wildoptions = { "exacttext", -- Discard regex artifacts when performing search pattern completion "pum", -- Show completions in a popup menu "tagfile", -- Show tag kind and file "fuzzy", -- Fuzzy matching (doesn't work with files/dirs, see `:help 'wildoptions'`, but patterns do work!) } -- Completion modes triggered in order by `wildtrigger()` or 'wildchar' vim.opt.wildmode = { "noselect", -- List matches without inserting "full", -- List matches and insert first full match } -- Insert unique match vim.keymap.set("c", string.format("%c", vim.o.wildchar), function() local complete_info = vim.fn.cmdcomplete_info() if complete_info.pum_visible == 1 then return #complete_info.matches == 1 and "" or "" end vim.fn.wildtrigger() end, { expr = true, desc = "Command line wildcard expansion" }) -- Show next completion choices after accepting an entry with `` vim.keymap.set("c", "", function() local complete_info = vim.fn.cmdcomplete_info() if complete_info.pum_visible == 1 then return complete_info.selected ~= -1 and "" or "" end return "" end, { expr = true })