From 285f6f498c8ba3ac0788edb1db2f8d2d3cb20fad Mon Sep 17 00:00:00 2001 From: jdrouhard Date: Wed, 20 Nov 2024 10:27:05 -0600 Subject: [PATCH] feat: extract word from completion item for auto-insert preview (#341) * feat: extract word from completion item for auto-insert preview Use a modified version of nvim-cmp's word extraction algorithm from expanded snippets for use in auto-insert preview textEdits. For example, if a snippet expansion results in something like: ``` my_neat_function(int arg1, double arg2) ``` Then the floating window will show the above for that completion, but when selecting it, the buffer will only insert `my_neat_function`. This allows using the snippet completion item without actually _accepting_ it by just selecting it in the preview window and then continuing to type. So one could type "my_n", Ctrl-N/Tab (or whatever your keymap is) to select `my_neat_function`, and then just continue with typing the ( and args manually. Of course, accepting the completion will still work as expected by expanding the snippet. * refactor: word extraction for auto-insert preview --------- Co-authored-by: Liam Dyer --- lua/blink/cmp/accept/preview.lua | 19 ++++------- lua/blink/cmp/utils.lua | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/lua/blink/cmp/accept/preview.lua b/lua/blink/cmp/accept/preview.lua index 0e6a4675..3dc4f90f 100644 --- a/lua/blink/cmp/accept/preview.lua +++ b/lua/blink/cmp/accept/preview.lua @@ -6,22 +6,17 @@ local function preview(item, previous_text_edit) -- with auto_insert, we may have to undo the previous preview if previous_text_edit ~= nil then text_edit.range = text_edits_lib.get_undo_range(previous_text_edit) end - -- for snippets, expand them with the default property names + if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then + local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(text_edit.newText) + text_edit.newText = require('blink.cmp.utils').get_prefix_before_brackets_and_quotes( + expanded_snippet and tostring(expanded_snippet) or text_edit.newText + ) + end + local cursor_pos = { text_edit.range.start.line + 1, text_edit.range.start.character + #text_edit.newText, } - if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then - local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(text_edit.newText) - text_edit.newText = expanded_snippet and tostring(expanded_snippet) or text_edit.newText - - -- place the cursor at the first tab stop - local tabstops = require('blink.cmp.sources.snippets.utils').get_tab_stops(text_edit.newText) - if tabstops and #tabstops > 0 then - cursor_pos[1] = text_edit.range.start.line + tabstops[1].line - cursor_pos[2] = text_edit.range.start.character + tabstops[1].character - end - end text_edits_lib.apply({ text_edit }) vim.api.nvim_win_set_cursor(0, cursor_pos) diff --git a/lua/blink/cmp/utils.lua b/lua/blink/cmp/utils.lua index cc0cb976..22481e1c 100644 --- a/lua/blink/cmp/utils.lua +++ b/lua/blink/cmp/utils.lua @@ -97,4 +97,61 @@ function utils.get_tailwind_hl(ctx) end end +local PAIRS_AND_INVALID_CHARS = {} +string.gsub('\'"=$()[]<>{} \t\n\r', '.', function(char) PAIRS_AND_INVALID_CHARS[string.byte(char)] = true end) + +local CLOSING_PAIR = { + [string.byte('<')] = string.byte('>'), + [string.byte('[')] = string.byte(']'), + [string.byte('(')] = string.byte(')'), + [string.byte('{')] = string.byte('}'), + [string.byte('"')] = string.byte('"'), + [string.byte("'")] = string.byte("'"), +} + +local ALPHANUMERIC = {} +string.gsub( + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', + '.', + function(char) ALPHANUMERIC[string.byte(char)] = true end +) + +--- Gets the prefix of the given text, stopping at brackets and quotes +--- @param text string +--- @return string +function utils.get_prefix_before_brackets_and_quotes(text) + local closing_pairs_stack = {} + local word = '' + + local add = function(char) + word = word .. string.char(char) + + -- if we've seen the opening pair, and we've just received the closing pair, + -- remove it from the closing pairs stack + if closing_pairs_stack[#closing_pairs_stack] == char then + table.remove(closing_pairs_stack, #closing_pairs_stack) + -- if the character is an opening pair, add it to the closing pairs stack + elseif CLOSING_PAIR[char] ~= nil then + table.insert(closing_pairs_stack, CLOSING_PAIR[char]) + end + end + + local has_alphanumeric = false + for i = 1, #text do + local char = string.byte(text, i) + if PAIRS_AND_INVALID_CHARS[char] == nil then + add(char) + has_alphanumeric = has_alphanumeric or ALPHANUMERIC[char] + elseif not has_alphanumeric or #closing_pairs_stack ~= 0 then + add(char) + -- if we had an alphanumeric, and the closing pairs stuck *just* emptied, + -- because the current character is a closing pair, we exit + if has_alphanumeric and #closing_pairs_stack == 0 then break end + else + break + end + end + return word +end + return utils