diff options
Diffstat (limited to '.config/nvim/plugin')
| -rw-r--r-- | .config/nvim/plugin/50-completion.lua | 139 | ||||
| -rw-r--r-- | .config/nvim/plugin/50-snippet.lua | 146 |
2 files changed, 146 insertions, 139 deletions
diff --git a/.config/nvim/plugin/50-completion.lua b/.config/nvim/plugin/50-completion.lua index 620d9bd..caf749d 100644 --- a/.config/nvim/plugin/50-completion.lua +++ b/.config/nvim/plugin/50-completion.lua @@ -36,145 +36,6 @@ vim.keymap.set({ "i", "s" }, "<C-p>", function() 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` diff --git a/.config/nvim/plugin/50-snippet.lua b/.config/nvim/plugin/50-snippet.lua new file mode 100644 index 0000000..612670f --- /dev/null +++ b/.config/nvim/plugin/50-snippet.lua @@ -0,0 +1,146 @@ +-- Jump between snippet placeholders, falling back to native behavior when no snippet is active +vim.keymap.set({ "i", "s" }, "<C-n>", function() + return vim.snippet.active({ direction = 1 }) and "<Cmd>lua vim.snippet.jump(1)<CR>" or "<C-n>" +end, { expr = true }) +vim.keymap.set({ "i", "s" }, "<C-p>", function() + return vim.snippet.active({ direction = -1 }) and "<Cmd>lua vim.snippet.jump(-1)<CR>" or "<C-p>" +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, +}) |
