From c58b3a8ec2cd71b422fbd4b1607e924996dfdebb Mon Sep 17 00:00:00 2001 From: Liam Dyer Date: Sun, 24 Nov 2024 17:56:48 -0500 Subject: [PATCH] feat: add `cancel` command for use with `auto_insert` related to #215 --- README.md | 4 ++- lua/blink/cmp/accept/preview.lua | 11 +++----- lua/blink/cmp/accept/text-edits.lua | 38 +++++++++++++++++++++----- lua/blink/cmp/config.lua | 3 +- lua/blink/cmp/init.lua | 9 ++++++ lua/blink/cmp/keymap.lua | 1 + lua/blink/cmp/windows/autocomplete.lua | 26 ++++++++++++++---- 7 files changed, 71 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 7fa5ab76..28b86e4a 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ MiniDeps.add({ -- When defining your own keymaps without a preset, no keybinds will be assigned automatically. -- -- Available commands: - -- show, hide, accept, select_and_accept, select_prev, select_next, show_documentation, hide_documentation, + -- show, hide, cancel, accept, select_and_accept, select_prev, select_next, show_documentation, hide_documentation, -- scroll_documentation_up, scroll_documentation_down, snippet_forward, snippet_backward, fallback -- -- "default" keymap @@ -428,6 +428,8 @@ MiniDeps.add({ -- 'preselect' will automatically select the first item in the completion list -- 'manual' will not select any item by default -- 'auto_insert' will not select any item by default, and insert the completion items automatically when selecting them + -- + -- When using 'auto_insert', you may want to bind a key to the `cancel` command, which will undo the selection selection = 'preselect', -- Controls how the completion items are rendered on the popup window draw = { diff --git a/lua/blink/cmp/accept/preview.lua b/lua/blink/cmp/accept/preview.lua index 3dc4f90f..26bf15fc 100644 --- a/lua/blink/cmp/accept/preview.lua +++ b/lua/blink/cmp/accept/preview.lua @@ -1,11 +1,8 @@ --- @param item blink.cmp.CompletionItem -local function preview(item, previous_text_edit) +local function preview(item) local text_edits_lib = require('blink.cmp.accept.text-edits') local text_edit = text_edits_lib.get_from_item(item) - -- 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 - 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( @@ -13,16 +10,16 @@ local function preview(item, previous_text_edit) ) 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 }) - vim.api.nvim_win_set_cursor(0, cursor_pos) - -- return so that it can be undone in the future - return text_edit + vim.api.nvim_win_set_cursor(0, cursor_pos) + return undo_text_edit end return preview diff --git a/lua/blink/cmp/accept/text-edits.lua b/lua/blink/cmp/accept/text-edits.lua index bbde9191..39dd5c22 100644 --- a/lua/blink/cmp/accept/text-edits.lua +++ b/lua/blink/cmp/accept/text-edits.lua @@ -7,6 +7,16 @@ function text_edits.apply(edits) vim.lsp.util.apply_text_edits(edits, vim.api.nv ------- Undo ------- +--- Gets the reverse of the text edit, must be called before applying +--- @param text_edit lsp.TextEdit +--- @return lsp.TextEdit +function text_edits.get_undo_text_edit(text_edit) + return { + range = text_edits.get_undo_range(text_edit), + newText = text_edits.get_text_to_replace(text_edit), + } +end + --- Gets the range for undoing an applied text edit --- @param text_edit lsp.TextEdit function text_edits.get_undo_range(text_edit) @@ -21,14 +31,28 @@ function text_edits.get_undo_range(text_edit) return range end ---- Undoes a text edit +--- Gets the text the text edit will replace --- @param text_edit lsp.TextEdit -function text_edits.undo(text_edit) - text_edit = vim.deepcopy(text_edit) - text_edit.range = text_edits.get_undo_range(text_edit) - text_edit.newText = '' - - text_edits.apply({ text_edit }) +--- @return string +function text_edits.get_text_to_replace(text_edit) + local bufnr = vim.api.nvim_get_current_buf() + local lines = {} + for line = text_edit.range.start.line, text_edit.range['end'].line do + local line_text = vim.api.nvim_buf_get_lines(bufnr, line, line + 1, false)[1] + local is_start_line = line == text_edit.range.start.line + local is_end_line = line == text_edit.range['end'].line + + if is_start_line and is_end_line then + table.insert(lines, line_text:sub(text_edit.range.start.character + 1, text_edit.range['end'].character)) + elseif is_start_line then + table.insert(lines, line_text:sub(text_edit.range.start.character + 1)) + elseif is_end_line then + table.insert(lines, line_text:sub(1, text_edit.range['end'].character)) + else + table.insert(lines, line_text) + end + end + return table.concat(lines, '\n') end ------- Get ------- diff --git a/lua/blink/cmp/config.lua b/lua/blink/cmp/config.lua index 713227e0..63ef386f 100644 --- a/lua/blink/cmp/config.lua +++ b/lua/blink/cmp/config.lua @@ -2,6 +2,7 @@ --- | 'fallback' Fallback to the built-in behavior --- | 'show' Show the completion window --- | 'hide' Hide the completion window +--- | 'cancel' Cancel the current completion, undoing the preview from auto_insert --- | 'accept' Accept the current completion item --- | 'select_and_accept' Select the current completion item and accept it --- | 'select_prev' Select the previous completion item @@ -204,7 +205,7 @@ local config = { -- When defining your own keymaps without a preset, no keybinds will be assigned automatically. -- -- Available commands: - -- show, hide, accept, select_and_accept, select_prev, select_next, show_documentation, hide_documentation, + -- show, hide, cancel, accept, select_and_accept, select_prev, select_next, show_documentation, hide_documentation, -- scroll_documentation_up, scroll_documentation_down, snippet_forward, snippet_backward, fallback -- -- "default" keymap diff --git a/lua/blink/cmp/init.lua b/lua/blink/cmp/init.lua index 1c3f67a6..3847c4f6 100644 --- a/lua/blink/cmp/init.lua +++ b/lua/blink/cmp/init.lua @@ -137,6 +137,15 @@ cmp.hide = function() return true end +cmp.cancel = function() + if not cmp.windows.autocomplete.win:is_open() then return end + vim.schedule(function() + cmp.windows.autocomplete.undo_preview() + cmp.trigger.hide() + end) + return true +end + --- @param callback fun(context: blink.cmp.Context) cmp.on_open = function(callback) cmp.windows.autocomplete.listen_on_open(callback) end diff --git a/lua/blink/cmp/keymap.lua b/lua/blink/cmp/keymap.lua index 994b68ab..ec35d09a 100644 --- a/lua/blink/cmp/keymap.lua +++ b/lua/blink/cmp/keymap.lua @@ -72,6 +72,7 @@ function keymap.setup(opts) local commands = { 'show', 'hide', + 'cancel', 'accept', 'select_and_accept', 'select_prev', diff --git a/lua/blink/cmp/windows/autocomplete.lua b/lua/blink/cmp/windows/autocomplete.lua index 92aaf79f..d6d6353f 100644 --- a/lua/blink/cmp/windows/autocomplete.lua +++ b/lua/blink/cmp/windows/autocomplete.lua @@ -12,6 +12,8 @@ --- @field auto_show boolean --- @field context blink.cmp.Context? --- @field event_targets blink.cmp.CompletionWindowEventTargets +--- @field preview_undo_text_edit? lsp.TextEdit +--- @field preview_context_id? number --- --- @field setup fun(): blink.cmp.CompletionWindow --- @@ -25,6 +27,7 @@ --- @field listen_on_position_update fun(callback: fun()) --- --- @field accept fun(): boolean? +--- @field undo_preview fun() --- --- @field select fun(line: number, skip_auto_insert?: boolean) --- @field select_next fun(opts?: { skip_auto_insert?: boolean }) @@ -200,8 +203,10 @@ function autocomplete.accept() if selected_item == nil then return end -- undo the preview if it exists - if autocomplete.preview_text_edit ~= nil and autocomplete.preview_context_id == autocomplete.context.id then - text_edits_lib.undo(autocomplete.preview_text_edit) + if autocomplete.preview_undo_text_edit ~= nil and autocomplete.preview_context_id == autocomplete.context.id then + text_edits_lib.apply({ autocomplete.preview_undo_text_edit }) + autocomplete.preview_undo_text_edit = nil + autocomplete.preview_context_id = nil end -- apply @@ -209,6 +214,14 @@ function autocomplete.accept() return true end +function autocomplete.undo_preview() + if autocomplete.preview_undo_text_edit ~= nil and autocomplete.preview_context_id == autocomplete.context.id then + text_edits_lib.apply({ autocomplete.preview_undo_text_edit }) + autocomplete.preview_undo_text_edit = nil + autocomplete.preview_context_id = nil + end +end + function autocomplete.select(line, skip_auto_insert) autocomplete.set_has_selected(true) vim.api.nvim_win_set_cursor(autocomplete.win:get_win(), { line, 0 }) @@ -218,9 +231,12 @@ function autocomplete.select(line, skip_auto_insert) -- when auto_insert is enabled, we immediately apply the text edit if config.windows.autocomplete.selection == 'auto_insert' and selected_item ~= nil and not skip_auto_insert then require('blink.cmp.trigger.completion').suppress_events_for_callback(function() - if autocomplete.preview_context_id ~= autocomplete.context.id then autocomplete.preview_text_edit = nil end - autocomplete.preview_text_edit = - require('blink.cmp.accept.preview')(selected_item, autocomplete.preview_text_edit) + -- undo the previous preview if it exists + if autocomplete.preview_context_id == autocomplete.context.id and autocomplete.preview_undo_text_edit ~= nil then + require('blink.cmp.accept.text-edits').apply({ autocomplete.preview_undo_text_edit }) + end + + autocomplete.preview_undo_text_edit = require('blink.cmp.accept.preview')(selected_item) autocomplete.preview_context_id = autocomplete.context.id end) end