diff --git a/lua/blink/cmp/accept/init.lua b/lua/blink/cmp/accept/init.lua index fa43de8b..ae06c9f6 100644 --- a/lua/blink/cmp/accept/init.lua +++ b/lua/blink/cmp/accept/init.lua @@ -1,24 +1,12 @@ local text_edits_lib = require('blink.cmp.accept.text-edits') local brackets_lib = require('blink.cmp.accept.brackets') -local config = require('blink.cmp.config') --- Applies a completion item to the current buffer --- @param item blink.cmp.CompletionItem local function accept(item) - local has_original_text_edit = item.textEdit ~= nil item = vim.deepcopy(item) item.textEdit = text_edits_lib.get_from_item(item) - -- As `text_edits.guess_text_edit`'s way of detecting the start position of the edit is a bit - -- naive for now, if selection mode is `auto_insert` and the LSP didn't provide a textEdit, then - -- we need to manually set the correct position here. - if config.windows.autocomplete.selection == 'auto_insert' and not has_original_text_edit then - local word = item.insertText or item.label - if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then word = item.label end - local current_col = vim.api.nvim_win_get_cursor(0)[2] - item.textEdit.range.start.character = current_col - #word - end - -- Add brackets to the text edit if needed local brackets_status, text_edit_with_brackets, offset = brackets_lib.add_brackets(vim.bo.filetype, item) item.textEdit = text_edit_with_brackets diff --git a/lua/blink/cmp/trigger/completion.lua b/lua/blink/cmp/trigger/completion.lua index 49179ed3..17836cab 100644 --- a/lua/blink/cmp/trigger/completion.lua +++ b/lua/blink/cmp/trigger/completion.lua @@ -2,12 +2,9 @@ -- (provided by the sources) or anything matching the `keyword_regex`, we create a new `context`. -- This can be used downstream to determine if we should make new requests to the sources or not. -local config = require('blink.cmp.config') -local config_trigger = config.trigger.completion +local config = require('blink.cmp.config').trigger.completion local sources = require('blink.cmp.sources.lib') local utils = require('blink.cmp.utils') -local autocomplete = require('blink.cmp.windows.autocomplete') -local text_edits_lib = require('blink.cmp.accept.text-edits') local trigger = { current_context_id = -1, @@ -32,31 +29,32 @@ function trigger.activate_autocmds() -- decide if we should show the completion window vim.api.nvim_create_autocmd('TextChangedI', { callback = function() + -- we were told to ignore the text changed event, so we update the context + -- but don't send an on_show event upstream + if trigger.ignore_next_text_changed then + if trigger.context ~= nil then trigger.show({ send_upstream = false }) end + trigger.ignore_next_text_changed = false + -- no characters added so let cursormoved handle it - if last_char == '' then return end + elseif last_char == '' then + return -- ignore if in a special buffer - if utils.is_special_buffer() then + elseif utils.is_special_buffer() then trigger.hide() - -- character forces a trigger according to the sources, create a fresh context + + -- character forces a trigger according to the sources, create a fresh context elseif vim.tbl_contains(sources.get_trigger_characters(), last_char) then trigger.context = nil trigger.triggered_by = nil trigger.show({ trigger_character = last_char }) - -- character is part of the current context OR in an existing context - elseif last_char:match(config_trigger.keyword_regex) ~= nil then + + -- character is part of the current context OR in an existing context + elseif last_char:match(config.keyword_regex) ~= nil then trigger.show() - -- nothing matches so hide - else - local selected_item = autocomplete.get_selected_item() - if - config.windows.autocomplete.selection == 'auto_insert' - and autocomplete.has_selected - and selected_item ~= nil - then - text_edits_lib.apply_additional_text_edits(selected_item) - end + -- nothing matches so hide + else trigger.hide() end @@ -66,6 +64,14 @@ function trigger.activate_autocmds() vim.api.nvim_create_autocmd({ 'CursorMovedI', 'InsertEnter' }, { callback = function(ev) + -- we were told to ignore the cursor moved event, so we update the context + -- but don't send an on_show event upstream + if trigger.ignore_next_cursor_moved and ev.event == 'CursorMovedI' then + if trigger.context ~= nil then trigger.show({ send_upstream = false }) end + trigger.ignore_next_cursor_moved = false + return + end + -- characters added so let textchanged handle it if last_char ~= '' then return end @@ -73,38 +79,34 @@ function trigger.activate_autocmds() local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) local is_on_trigger = vim.tbl_contains(sources.get_trigger_characters(), char_under_cursor) local is_on_trigger_for_show_on_insert = is_on_trigger - and not vim.tbl_contains(config_trigger.show_on_insert_blocked_trigger_characters, char_under_cursor) - local is_on_context_char = char_under_cursor:match(config_trigger.keyword_regex) ~= nil + and not vim.tbl_contains(config.show_on_insert_blocked_trigger_characters, char_under_cursor) + local is_on_context_char = char_under_cursor:match(config.keyword_regex) ~= nil - local insert_enter_on_trigger_character = config_trigger.show_on_insert_on_trigger_character + local insert_enter_on_trigger_character = config.show_on_insert_on_trigger_character and is_on_trigger_for_show_on_insert and ev.event == 'InsertEnter' - if config.windows.autocomplete.selection ~= 'auto_insert' or trigger.triggered_by ~= 'select' then - -- check if we're still within the bounds of the query used for the context - if trigger.within_query_bounds(vim.api.nvim_win_get_cursor(0)) then - trigger.show() + -- check if we're still within the bounds of the query used for the context + if trigger.within_query_bounds(vim.api.nvim_win_get_cursor(0)) then + trigger.show() -- check if we've entered insert mode on a trigger character -- or if we've moved onto a trigger character - elseif insert_enter_on_trigger_character or (is_on_trigger and trigger.context ~= nil) then - trigger.context = nil - trigger.triggered_by = nil - trigger.show({ trigger_character = char_under_cursor }) + elseif insert_enter_on_trigger_character or (is_on_trigger and trigger.context ~= nil) then + trigger.context = nil + trigger.triggered_by = nil + trigger.show({ trigger_character = char_under_cursor }) -- show if we currently have a context, and we've moved outside of it's bounds by 1 char - elseif is_on_context_char and trigger.context ~= nil and cursor_col == trigger.context.bounds.start_col - 1 then - trigger.context = nil - trigger.triggered_by = nil - trigger.show() + elseif is_on_context_char and trigger.context ~= nil and cursor_col == trigger.context.bounds.start_col - 1 then + trigger.context = nil + trigger.triggered_by = nil + trigger.show() -- otherwise hide - else - trigger.hide() - end + else + trigger.hide() end - - trigger.triggered_by = nil end, }) @@ -125,7 +127,26 @@ function trigger.activate_autocmds() return trigger end ---- @param opts { trigger_character: string } | nil +-- todo: extract into an autocmd module +-- hack: there's likely edge cases with this since we can't know for sure +-- if the autocmds will fire for cursor_moved afaik +function trigger.ignore_autocmds_for_callback(cb) + local cursor_before = vim.api.nvim_win_get_cursor(0) + local changed_tick_before = vim.api.nvim_buf_get_changedtick(0) + + cb() + + local cursor_after = vim.api.nvim_win_get_cursor(0) + local changed_tick_after = vim.api.nvim_buf_get_changedtick(0) + + local is_insert_mode = vim.api.nvim_get_mode().mode == 'i' + trigger.ignore_next_text_changed = changed_tick_after ~= changed_tick_before and is_insert_mode + -- todo: does this guarantee that the CursorMovedI event will fire? + trigger.ignore_next_cursor_moved = (cursor_after[1] ~= cursor_before[1] or cursor_after[2] ~= cursor_before[2]) + and is_insert_mode +end + +--- @param opts { trigger_character?: string, send_upstream?: boolean } | nil function trigger.show(opts) opts = opts or {} @@ -142,7 +163,7 @@ function trigger.show(opts) bufnr = vim.api.nvim_get_current_buf(), cursor = cursor, line = vim.api.nvim_buf_get_lines(0, cursor[1] - 1, cursor[1], false)[1], - bounds = trigger.get_context_bounds(config_trigger.keyword_regex), + bounds = trigger.get_context_bounds(config.keyword_regex), trigger = { kind = opts.trigger_character and vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter or vim.lsp.protocol.CompletionTriggerKind.Invoked, @@ -150,7 +171,7 @@ function trigger.show(opts) }, } - trigger.event_targets.on_show(trigger.context) + if opts.send_upstream ~= false then trigger.event_targets.on_show(trigger.context) end end --- @param callback fun(context: blink.cmp.Context) diff --git a/lua/blink/cmp/windows/autocomplete.lua b/lua/blink/cmp/windows/autocomplete.lua index 0980b3ca..3a009dc6 100644 --- a/lua/blink/cmp/windows/autocomplete.lua +++ b/lua/blink/cmp/windows/autocomplete.lua @@ -160,8 +160,6 @@ end --- @param line number local function select(line) - local auto_insert = config.windows.autocomplete.selection == 'auto_insert' - local prev_selected_item = autocomplete.get_selected_item() autocomplete.set_has_selected(true) @@ -169,25 +167,29 @@ local function select(line) local selected_item = autocomplete.get_selected_item() - if auto_insert and selected_item ~= nil then - local text_edit = text_edits_lib.get_from_item(selected_item) - - if selected_item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then - text_edit.newText = selected_item.label - end - - if - prev_selected_item ~= nil and prev_selected_item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet - then - local current_col = vim.api.nvim_win_get_cursor(0)[2] - text_edit.range.start.character = current_col - #prev_selected_item.label - end - - text_edits_lib.apply_text_edits(selected_item.client_id, { text_edit }) - vim.api.nvim_win_set_cursor(0, { - text_edit.range.start.line + 1, - text_edit.range.start.character + #text_edit.newText, - }) + -- when auto_insert is enabled, we immediately apply the text edit + -- todo: move this to the accept module + if config.windows.autocomplete.selection == 'auto_insert' and selected_item ~= nil then + require('blink.cmp.trigger.completion').ignore_autocmds_for_callback(function() + local text_edit = text_edits_lib.get_from_item(selected_item) + + if selected_item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then + text_edit.newText = selected_item.label + end + + if + prev_selected_item ~= nil and prev_selected_item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet + then + local current_col = vim.api.nvim_win_get_cursor(0)[2] + text_edit.range.start.character = current_col - #prev_selected_item.label + end + + text_edits_lib.apply_text_edits(selected_item.client_id, { text_edit }) + vim.api.nvim_win_set_cursor(0, { + text_edit.range.start.line + 1, + text_edit.range.start.character + #text_edit.newText, + }) + end) end autocomplete.event_targets.on_select(selected_item, autocomplete.context)