Skip to content

Commit 1df7d33

Browse files
noomlySaghen
andauthored
feat!: implement auto-insert option (#65)
* feat: implement auto-insert option * doc: revert README.md formatting * fix: properly handle auto-inserting when context length is zero * fix: deduplicate selection logic and auto-inserting while in mid-bound * fix: improve last fix * fix: switch to using much more reliable text-exits for auto-insert * fix: guess_text_edit not handling 0 indexed column * fix: use item's label when auto-inserting snippets * feat!: change config of auto_insert,preselect to a single 'selection' * feat: apply additional text edits when implicitely accepting completion * fix: accepting an auto-inserted bugs when item contains special characters * fix: completion does not hide after accepting while selection~=auto_insert * feat: ignore autocmds wrapper for trigger * chore: clear remaining triggered_by code --------- Co-authored-by: Liam Dyer <[email protected]>
1 parent 27903be commit 1df7d33

File tree

5 files changed

+109
-30
lines changed

5 files changed

+109
-30
lines changed

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,11 @@ MiniDeps.add({
286286
-- which directions to show the window,
287287
-- falling back to the next direction when there's not enough space
288288
direction_priority = { 's', 'n' },
289-
-- whether to preselect the first item in the completion list
290-
preselect = true,
289+
-- Controls how the completion items are selected
290+
-- 'preselect' will automatically select the first item in the completion list
291+
-- 'manual' will not select any item by default
292+
-- 'auto_insert' will not select any item by default, and insert the completion items automatically when selecting them
293+
selection = 'preselect',
291294
-- Controls how the completion items are rendered on the popup window
292295
-- 'simple' will render the item's kind icon the left alongside the label
293296
-- 'reversed' will render the label on the left and the kind icon + name on the right

lua/blink/cmp/accept/text-edits.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ function text_edits.get_from_item(item)
77
-- from when the items were fetched versus the current.
88
-- hack: is there a better way?
99
if item.textEdit ~= nil then
10-
local text_edit = utils.shallow_copy(item.textEdit)
10+
local text_edit = vim.deepcopy(item.textEdit)
1111
local offset = vim.api.nvim_win_get_cursor(0)[2] - item.cursor_column
1212
text_edit.range['end'].character = text_edit.range['end'].character + offset
1313
return text_edit

lua/blink/cmp/config.lua

+6-3
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
--- @field border? blink.cmp.WindowBorder
9090
--- @field order? "top_down" | "bottom_up"
9191
--- @field direction_priority? ("n" | "s")[]
92-
--- @field preselect? boolean
92+
--- @field selection? "preselect" | "manual" | "auto_insert"
9393
--- @field winhighlight? string
9494
--- @field scrolloff? number
9595
--- @field draw? 'simple' | 'reversed' | 'minimal' | function(blink.cmp.CompletionRenderContext): blink.cmp.Component[]
@@ -250,8 +250,11 @@ local config = {
250250
-- which directions to show the window,
251251
-- falling back to the next direction when there's not enough space
252252
direction_priority = { 's', 'n' },
253-
-- whether to preselect the first item in the completion list
254-
preselect = true,
253+
-- Controls how the completion items are selected
254+
-- 'preselect' will automatically select the first item in the completion list
255+
-- 'manual' will not select any item by default
256+
-- 'auto_insert' will not select any item by default, and insert the completion items automatically when selecting them
257+
selection = 'preselect',
255258
-- Controls how the completion items are rendered on the popup window
256259
-- 'simple' will render the item's kind icon the left alongside the label
257260
-- 'reversed' will render the label on the left and the kind icon + name on the right

lua/blink/cmp/trigger/completion.lua

+50-13
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,29 @@ function trigger.activate_autocmds()
2727
-- decide if we should show the completion window
2828
vim.api.nvim_create_autocmd('TextChangedI', {
2929
callback = function()
30+
-- we were told to ignore the text changed event, so we update the context
31+
-- but don't send an on_show event upstream
32+
if trigger.ignore_next_text_changed then
33+
if trigger.context ~= nil then trigger.show({ send_upstream = false }) end
34+
trigger.ignore_next_text_changed = false
35+
3036
-- no characters added so let cursormoved handle it
31-
if last_char == '' then return end
37+
elseif last_char == '' then
38+
return
3239

3340
-- ignore if in a special buffer
34-
if utils.is_special_buffer() then
41+
elseif utils.is_special_buffer() then
3542
trigger.hide()
43+
3644
-- character forces a trigger according to the sources, create a fresh context
3745
elseif vim.tbl_contains(sources.get_trigger_characters(), last_char) then
3846
trigger.context = nil
3947
trigger.show({ trigger_character = last_char })
48+
4049
-- character is part of the current context OR in an existing context
4150
elseif last_char:match(config.keyword_regex) ~= nil then
4251
trigger.show()
52+
4353
-- nothing matches so hide
4454
else
4555
trigger.hide()
@@ -51,6 +61,14 @@ function trigger.activate_autocmds()
5161

5262
vim.api.nvim_create_autocmd({ 'CursorMovedI', 'InsertEnter' }, {
5363
callback = function(ev)
64+
-- we were told to ignore the cursor moved event, so we update the context
65+
-- but don't send an on_show event upstream
66+
if trigger.ignore_next_cursor_moved and ev.event == 'CursorMovedI' then
67+
if trigger.context ~= nil then trigger.show({ send_upstream = false }) end
68+
trigger.ignore_next_cursor_moved = false
69+
return
70+
end
71+
5472
-- characters added so let textchanged handle it
5573
if last_char ~= '' then return end
5674

@@ -61,25 +79,26 @@ function trigger.activate_autocmds()
6179
and not vim.tbl_contains(config.show_on_insert_blocked_trigger_characters, char_under_cursor)
6280
local is_on_context_char = char_under_cursor:match(config.keyword_regex) ~= nil
6381

82+
local insert_enter_on_trigger_character = config.show_on_insert_on_trigger_character
83+
and is_on_trigger_for_show_on_insert
84+
and ev.event == 'InsertEnter'
85+
6486
-- check if we're still within the bounds of the query used for the context
6587
if trigger.within_query_bounds(vim.api.nvim_win_get_cursor(0)) then
6688
trigger.show()
6789

68-
-- check if we've entered insert mode on a trigger character
69-
-- or if we've moved onto a trigger character
70-
elseif
71-
(config.show_on_insert_on_trigger_character and is_on_trigger_for_show_on_insert and ev.event == 'InsertEnter')
72-
or (is_on_trigger and trigger.context ~= nil)
73-
then
90+
-- check if we've entered insert mode on a trigger character
91+
-- or if we've moved onto a trigger character
92+
elseif insert_enter_on_trigger_character or (is_on_trigger and trigger.context ~= nil) then
7493
trigger.context = nil
7594
trigger.show({ trigger_character = char_under_cursor })
7695

77-
-- show if we currently have a context, and we've moved outside of it's bounds by 1 char
96+
-- show if we currently have a context, and we've moved outside of it's bounds by 1 char
7897
elseif is_on_context_char and trigger.context ~= nil and cursor_col == trigger.context.bounds.start_col - 1 then
7998
trigger.context = nil
8099
trigger.show()
81100

82-
-- otherwise hide
101+
-- otherwise hide
83102
else
84103
trigger.hide()
85104
end
@@ -103,7 +122,26 @@ function trigger.activate_autocmds()
103122
return trigger
104123
end
105124

106-
--- @param opts { trigger_character: string } | nil
125+
-- todo: extract into an autocmd module
126+
-- hack: there's likely edge cases with this since we can't know for sure
127+
-- if the autocmds will fire for cursor_moved afaik
128+
function trigger.ignore_autocmds_for_callback(cb)
129+
local cursor_before = vim.api.nvim_win_get_cursor(0)
130+
local changed_tick_before = vim.api.nvim_buf_get_changedtick(0)
131+
132+
cb()
133+
134+
local cursor_after = vim.api.nvim_win_get_cursor(0)
135+
local changed_tick_after = vim.api.nvim_buf_get_changedtick(0)
136+
137+
local is_insert_mode = vim.api.nvim_get_mode().mode == 'i'
138+
trigger.ignore_next_text_changed = changed_tick_after ~= changed_tick_before and is_insert_mode
139+
-- todo: does this guarantee that the CursorMovedI event will fire?
140+
trigger.ignore_next_cursor_moved = (cursor_after[1] ~= cursor_before[1] or cursor_after[2] ~= cursor_before[2])
141+
and is_insert_mode
142+
end
143+
144+
--- @param opts { trigger_character?: string, send_upstream?: boolean } | nil
107145
function trigger.show(opts)
108146
opts = opts or {}
109147

@@ -128,7 +166,7 @@ function trigger.show(opts)
128166
},
129167
}
130168

131-
trigger.event_targets.on_show(trigger.context)
169+
if opts.send_upstream ~= false then trigger.event_targets.on_show(trigger.context) end
132170
end
133171

134172
--- @param callback fun(context: blink.cmp.Context)
@@ -138,7 +176,6 @@ function trigger.hide()
138176
if not trigger.context then return end
139177

140178
trigger.context = nil
141-
142179
trigger.event_targets.on_hide()
143180
end
144181

lua/blink/cmp/windows/autocomplete.lua

+47-11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
local config = require('blink.cmp.config')
99
local renderer = require('blink.cmp.windows.lib.render')
10+
local text_edits_lib = require('blink.cmp.accept.text-edits')
1011
local autocmp_config = config.windows.autocomplete
1112
local autocomplete = {
1213
---@type blink.cmp.CompletionItem[]
@@ -74,7 +75,7 @@ function autocomplete.open_with_items(context, items)
7475

7576
autocomplete.context = context
7677
autocomplete.update_position(context)
77-
autocomplete.set_has_selected(autocmp_config.preselect)
78+
autocomplete.set_has_selected(autocmp_config.selection == 'preselect')
7879

7980
-- todo: some logic to maintain the selection if the user moved the cursor?
8081
vim.api.nvim_win_set_cursor(autocomplete.win:get_win(), { 1, 0 })
@@ -85,13 +86,13 @@ function autocomplete.open()
8586
if autocomplete.win:is_open() then return end
8687
vim.iter(autocomplete.event_targets.on_open):each(function(callback) callback() end)
8788
autocomplete.win:open()
88-
autocomplete.set_has_selected(autocmp_config.preselect)
89+
autocomplete.set_has_selected(autocmp_config.selection == 'preselect')
8990
end
9091

9192
function autocomplete.close()
9293
if not autocomplete.win:is_open() then return end
9394
autocomplete.win:close()
94-
autocomplete.has_selected = autocmp_config.preselect
95+
autocomplete.set_has_selected(autocmp_config.selection == 'preselect')
9596

9697
vim.iter(autocomplete.event_targets.on_close):each(function(callback) callback() end)
9798
end
@@ -157,6 +158,43 @@ end
157158

158159
---------- Selection ----------
159160

161+
--- @param line number
162+
local function select(line)
163+
local prev_selected_item = autocomplete.get_selected_item()
164+
165+
autocomplete.set_has_selected(true)
166+
vim.api.nvim_win_set_cursor(autocomplete.win:get_win(), { line, 0 })
167+
168+
local selected_item = autocomplete.get_selected_item()
169+
170+
-- when auto_insert is enabled, we immediately apply the text edit
171+
-- todo: move this to the accept module
172+
if config.windows.autocomplete.selection == 'auto_insert' and selected_item ~= nil then
173+
require('blink.cmp.trigger.completion').ignore_autocmds_for_callback(function()
174+
local text_edit = text_edits_lib.get_from_item(selected_item)
175+
176+
if selected_item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then
177+
text_edit.newText = selected_item.label
178+
end
179+
180+
if
181+
prev_selected_item ~= nil and prev_selected_item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet
182+
then
183+
local current_col = vim.api.nvim_win_get_cursor(0)[2]
184+
text_edit.range.start.character = current_col - #prev_selected_item.label
185+
end
186+
187+
text_edits_lib.apply_text_edits(selected_item.client_id, { text_edit })
188+
vim.api.nvim_win_set_cursor(0, {
189+
text_edit.range.start.line + 1,
190+
text_edit.range.start.character + #text_edit.newText,
191+
})
192+
end)
193+
end
194+
195+
autocomplete.event_targets.on_select(selected_item, autocomplete.context)
196+
end
197+
160198
function autocomplete.select_next()
161199
if not autocomplete.win:is_open() then return end
162200

@@ -166,6 +204,7 @@ function autocomplete.select_next()
166204
-- We need to ajust the disconnect between the line position
167205
-- on the window and the selected item
168206
if not autocomplete.has_selected then line = line - 1 end
207+
if autocomplete.has_selected and l == 1 then return end
169208
if line == l then
170209
-- at the end of completion list and the config is not enabled: do nothing
171210
if not cycle_from_bottom then return end
@@ -174,10 +213,7 @@ function autocomplete.select_next()
174213
line = line + 1
175214
end
176215

177-
autocomplete.set_has_selected(true)
178-
179-
vim.api.nvim_win_set_cursor(autocomplete.win:get_win(), { line, 0 })
180-
autocomplete.event_targets.on_select(autocomplete.get_selected_item(), autocomplete.context)
216+
select(line)
181217
end
182218

183219
function autocomplete.select_prev()
@@ -186,17 +222,15 @@ function autocomplete.select_prev()
186222
local cycle_from_top = config.windows.autocomplete.cycle.from_top
187223
local l = #autocomplete.items
188224
local line = vim.api.nvim_win_get_cursor(autocomplete.win:get_win())[1]
225+
if autocomplete.has_selected and l == 1 then return end
189226
if line <= 1 then
190227
if not cycle_from_top then return end
191228
line = l
192229
else
193230
line = line - 1
194231
end
195232

196-
autocomplete.set_has_selected(true)
197-
198-
vim.api.nvim_win_set_cursor(autocomplete.win:get_win(), { line, 0 })
199-
autocomplete.event_targets.on_select(autocomplete.get_selected_item(), autocomplete.context)
233+
select(line)
200234
end
201235

202236
function autocomplete.listen_on_select(callback) autocomplete.event_targets.on_select = callback end
@@ -289,6 +323,8 @@ function autocomplete.render_item_reversed(ctx)
289323
}
290324
end
291325

326+
--- @param ctx blink.cmp.CompletionRenderContext
327+
--- @return blink.cmp.Component[]
292328
function autocomplete.render_item_minimal(ctx)
293329
return {
294330
' ',

0 commit comments

Comments
 (0)