From 7cb95de38967a3d2dd403ede97213ae5b22d630d Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Sun, 7 Jun 2026 16:48:18 +0200 Subject: feat(nvim): support custom snippets --- .config/nvim/after/ftplugin/lua.lua | 11 +++++ .config/nvim/plugin/50-completion.lua | 80 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) (limited to '.config/nvim') diff --git a/.config/nvim/after/ftplugin/lua.lua b/.config/nvim/after/ftplugin/lua.lua index 05e4978..b7d29c1 100644 --- a/.config/nvim/after/ftplugin/lua.lua +++ b/.config/nvim/after/ftplugin/lua.lua @@ -28,7 +28,18 @@ vim.opt_local.textwidth = vim.g.dotfiles.textwidth.lua -- gw wraps at this value vim.opt_local.colorcolumn = "+1" -- Highlight one column after 'textwidth' vim.opt_local.complete = { "o", -- 'omnifunc' + "F", -- 'completefunc' (snippet source, see plugin/50-completion.lua) } vim.b.autoformat = true vim.b.format_func = format + +------------------------------------------------------------------------------------------------------------------------ +-- Snippets +------------------------------------------------------------------------------------------------------------------------ +-- Trigger word -> LSP snippet body. `$1` mirrors the `${1:X}` placeholder, so the variable is typed once. +-- Surfaced in the completion menu via the "F" source in 'complete' above (see plugin/50-completion.lua). +vim.b.snippets = { + ["debug_print"] = 'vim.print("${1:X} = " .. vim.inspect($1))', +} +vim.bo.completefunc = "v:lua.dotfiles_snippet_source" diff --git a/.config/nvim/plugin/50-completion.lua b/.config/nvim/plugin/50-completion.lua index 56c99d7..f21b1f4 100644 --- a/.config/nvim/plugin/50-completion.lua +++ b/.config/nvim/plugin/50-completion.lua @@ -91,6 +91,86 @@ snippet.expand = function(input) 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 ------------------------------------------------------------------------------------------------------------------------ -- cgit v1.3.1