Skip to content

Commit

Permalink
feat: extract word from completion item for auto-insert preview (#341)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
jdrouhard and Saghen authored Nov 20, 2024
1 parent 9ad8886 commit 285f6f4
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 12 deletions.
19 changes: 7 additions & 12 deletions lua/blink/cmp/accept/preview.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions lua/blink/cmp/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 285f6f4

Please sign in to comment.