summaryrefslogtreecommitdiff
path: root/lua/blink/cmp/completion/accept
diff options
context:
space:
mode:
authorMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
committerMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
commitb77413ff8f59f380612074f0c9bd49093d8db695 (patch)
tree32c39a811ba96ed4ab0a1c81cce9f8d518ed7e31 /lua/blink/cmp/completion/accept
Squashed 'mut/neovim/pack/plugins/start/blink.cmp/' content from commit 1cc3b1a
git-subtree-dir: mut/neovim/pack/plugins/start/blink.cmp git-subtree-split: 1cc3b1a908fbcfd15451c4772759549724f38524
Diffstat (limited to 'lua/blink/cmp/completion/accept')
-rw-r--r--lua/blink/cmp/completion/accept/init.lua110
-rw-r--r--lua/blink/cmp/completion/accept/prefix.lua58
-rw-r--r--lua/blink/cmp/completion/accept/preview.lua35
3 files changed, 203 insertions, 0 deletions
diff --git a/lua/blink/cmp/completion/accept/init.lua b/lua/blink/cmp/completion/accept/init.lua
new file mode 100644
index 0000000..c4041d3
--- /dev/null
+++ b/lua/blink/cmp/completion/accept/init.lua
@@ -0,0 +1,110 @@
+local text_edits_lib = require('blink.cmp.lib.text_edits')
+local brackets_lib = require('blink.cmp.completion.brackets')
+
+--- Applies a completion item to the current buffer
+--- @param ctx blink.cmp.Context
+--- @param item blink.cmp.CompletionItem
+--- @param callback fun()
+local function accept(ctx, item, callback)
+ local sources = require('blink.cmp.sources.lib')
+ require('blink.cmp.completion.trigger').hide()
+
+ -- Start the resolve immediately since text changes can invalidate the item
+ -- with some LSPs (i.e. rust-analyzer) causing them to return the item as-is
+ -- without i.e. auto-imports
+ sources
+ .resolve(ctx, item)
+ :map(function(item)
+ item = vim.deepcopy(item)
+
+ -- Get additional text edits, converted to utf-8
+ local all_text_edits = vim.deepcopy(item.additionalTextEdits or {})
+ all_text_edits = vim.tbl_map(
+ function(text_edit) return text_edits_lib.to_utf_8(text_edit, text_edits_lib.offset_encoding_from_item(item)) end,
+ all_text_edits
+ )
+
+ -- TODO: it's not obvious that this is converting to utf-8
+ item.textEdit = text_edits_lib.get_from_item(item)
+
+ -- Create an undo point, if it's not a snippet, since the snippet engine should handle undo
+ if
+ ctx.mode == 'default'
+ and require('blink.cmp.config').completion.accept.create_undo_point
+ and item.insertTextFormat ~= vim.lsp.protocol.InsertTextFormat.Snippet
+ -- HACK: We check the kind here because the Luasnip source returns PlainText and handles
+ -- expansion itself. Otherwise, Luasnip will fail to enter select mode
+ -- https://github.com/Saghen/blink.cmp/commit/284dd37f9bbc632f8281d6361e877db5b45e6ff0#r150498482
+ and item.kind ~= require('blink.cmp.types').CompletionItemKind.Snippet
+ then
+ -- setting the undolevels forces neovim to create an undo point
+ vim.o.undolevels = vim.o.undolevels
+ end
+
+ -- Ignore snippets that only contain text
+ -- FIXME: doesn't handle escaped snippet placeholders "\\$1" should output "$1", not "\$1"
+ if
+ item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet
+ and item.kind ~= require('blink.cmp.types').CompletionItemKind.Snippet
+ then
+ local parsed_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(item.textEdit.newText)
+ if
+ parsed_snippet ~= nil
+ and #parsed_snippet.data.children == 1
+ and parsed_snippet.data.children[1].type == vim.lsp._snippet_grammar.NodeType.Text
+ then
+ item.insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText
+ end
+ end
+
+ -- Add brackets to the text edit if needed
+ local brackets_status, text_edit_with_brackets, offset = brackets_lib.add_brackets(ctx, vim.bo.filetype, item)
+ item.textEdit = text_edit_with_brackets
+
+ -- Snippet
+ if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then
+ assert(ctx.mode == 'default', 'Snippets are only supported in default mode')
+
+ -- We want to handle offset_encoding and the text edit api can do this for us
+ -- so we empty the newText and apply
+ local temp_text_edit = vim.deepcopy(item.textEdit)
+ temp_text_edit.newText = ''
+ table.insert(all_text_edits, temp_text_edit)
+ text_edits_lib.apply(all_text_edits)
+
+ -- Expand the snippet
+ require('blink.cmp.config').snippets.expand(item.textEdit.newText)
+
+ -- OR Normal: Apply the text edit and move the cursor
+ else
+ table.insert(all_text_edits, item.textEdit)
+ text_edits_lib.apply(all_text_edits)
+ -- TODO: should move the cursor only by the offset since text edit handles everything else?
+ ctx.set_cursor({ ctx.get_cursor()[1], item.textEdit.range.start.character + #item.textEdit.newText + offset })
+ end
+
+ -- Let the source execute the item itself
+ sources.execute(ctx, item):map(function()
+ -- Check semantic tokens for brackets, if needed, and apply additional text edits
+ if brackets_status == 'check_semantic_token' then
+ -- TODO: since we apply the additional text edits after, auto imported functions will not
+ -- get auto brackets. If we apply them before, we have to modify the textEdit to compensate
+ brackets_lib.add_brackets_via_semantic_token(vim.bo.filetype, item, function()
+ require('blink.cmp.completion.trigger').show_if_on_trigger_character({ is_accept = true })
+ require('blink.cmp.signature.trigger').show_if_on_trigger_character()
+ callback()
+ end)
+ else
+ require('blink.cmp.completion.trigger').show_if_on_trigger_character({ is_accept = true })
+ require('blink.cmp.signature.trigger').show_if_on_trigger_character()
+ callback()
+ end
+
+ -- Notify the rust module that the item was accessed
+ require('blink.cmp.fuzzy').access(item)
+ end)
+ end)
+ :catch(function(err) vim.notify(err, vim.log.levels.ERROR, { title = 'blink.cmp' }) end)
+end
+
+return accept
diff --git a/lua/blink/cmp/completion/accept/prefix.lua b/lua/blink/cmp/completion/accept/prefix.lua
new file mode 100644
index 0000000..3c51715
--- /dev/null
+++ b/lua/blink/cmp/completion/accept/prefix.lua
@@ -0,0 +1,58 @@
+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
+local function 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 stack *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 get_prefix_before_brackets_and_quotes
diff --git a/lua/blink/cmp/completion/accept/preview.lua b/lua/blink/cmp/completion/accept/preview.lua
new file mode 100644
index 0000000..88b46e2
--- /dev/null
+++ b/lua/blink/cmp/completion/accept/preview.lua
@@ -0,0 +1,35 @@
+--- @param item blink.cmp.CompletionItem
+--- @return lsp.TextEdit undo_text_edit, integer[]? undo_cursor_pos The text edit to apply and the original cursor
+--- position to move to when undoing the preview,
+local function preview(item)
+ local text_edits_lib = require('blink.cmp.lib.text_edits')
+ local text_edit = text_edits_lib.get_from_item(item)
+
+ if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then
+ local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(text_edit.newText)
+ local snippet = expanded_snippet and tostring(expanded_snippet) or text_edit.newText
+ local get_prefix_before_brackets_and_quotes = require('blink.cmp.completion.accept.prefix')
+ text_edit.newText = get_prefix_before_brackets_and_quotes(snippet)
+ end
+
+ local undo_text_edit = text_edits_lib.get_undo_text_edit(text_edit)
+ local cursor_pos = {
+ text_edit.range.start.line + 1,
+ text_edit.range.start.character + #text_edit.newText,
+ }
+
+ text_edits_lib.apply({ text_edit })
+
+ local original_cursor = vim.api.nvim_win_get_cursor(0)
+ local cursor_moved = false
+
+ -- TODO: remove when text_edits_lib.apply begins setting cursor position
+ if vim.api.nvim_get_mode().mode ~= 'c' then
+ vim.api.nvim_win_set_cursor(0, cursor_pos)
+ cursor_moved = true
+ end
+
+ return undo_text_edit, cursor_moved and original_cursor or nil
+end
+
+return preview