Skip to content

Commit c0d1824

Browse files
authored
fix: prevent partial accepts from triggering a new suggestion (#469)
Allows users to quickly accept a series of words or lines without waiting for the suggestion every time Fixes #467
1 parent dc579f9 commit c0d1824

12 files changed

+636
-30
lines changed

lua/copilot/api/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ function M.notify_shown(client, params, callback)
130130
return M.request(client, "notifyShown", params, callback)
131131
end
132132

133-
---@alias copilot_get_completions_data_completion { displayText: string, position: { character: integer, line: integer }, range: { ['end']: { character: integer, line: integer }, start: { character: integer, line: integer } }, text: string, uuid: string }
133+
---@alias copilot_get_completions_data_completion { displayText: string, position: { character: integer, line: integer }, range: { ['end']: { character: integer, line: integer }, start: { character: integer, line: integer } }, text: string, uuid: string, partial_text: string }
134134
---@alias copilot_get_completions_data { completions: copilot_get_completions_data_completion[] }
135135

136136
---@return any|nil err

lua/copilot/suggestion/init.lua

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ local utils = require("copilot.client.utils")
99

1010
local M = {}
1111

12-
---@alias copilot_suggestion_context { first?: integer, cycling?: integer, cycling_callbacks?: (fun(ctx: copilot_suggestion_context):nil)[], params?: table, suggestions?: copilot_get_completions_data_completion[], choice?: integer, shown_choices?: table<string, true> }
12+
---@alias copilot_suggestion_context { first?: integer, cycling?: integer, cycling_callbacks?: (fun(ctx: copilot_suggestion_context):nil)[], params?: table, suggestions?: copilot_get_completions_data_completion[], choice?: integer, shown_choices?: table<string, true>, accepted_partial?: boolean }
1313

1414
local copilot = {
1515
setup_done = false,
@@ -26,6 +26,8 @@ local copilot = {
2626
debounce = 75,
2727
}
2828

29+
local ignore_next_cursor_moved = false
30+
2931
local function with_client(fn)
3032
local client = c.get()
3133
if client then
@@ -58,6 +60,28 @@ local function get_ctx(bufnr)
5860
return ctx
5961
end
6062

63+
---@param idx integer
64+
---@param new_line integer
65+
---@param new_end_col integer
66+
---@param bufnr? integer
67+
local function update_ctx_suggestion_position(idx, new_line, new_end_col, bufnr)
68+
bufnr = bufnr or vim.api.nvim_get_current_buf()
69+
70+
if not copilot.context[bufnr] then
71+
return
72+
end
73+
74+
if not copilot.context[bufnr].suggestions[idx] then
75+
return
76+
end
77+
78+
local suggestion = copilot.context[bufnr].suggestions[idx]
79+
suggestion.range["start"].line = new_line
80+
suggestion.range["start"].character = 0
81+
suggestion.range["end"].line = new_line
82+
suggestion.range["end"].character = new_end_col
83+
end
84+
6185
---@param idx integer
6286
---@param text string
6387
---@param bufnr? integer
@@ -89,6 +113,7 @@ local function reset_ctx(ctx)
89113
ctx.suggestions = nil
90114
ctx.choice = nil
91115
ctx.shown_choices = nil
116+
ctx.accepted_partial = nil
92117
end
93118

94119
local function set_keymap(keymap)
@@ -249,10 +274,10 @@ local function get_current_suggestion(ctx)
249274
return nil
250275
end
251276

252-
if choice.range.start.character ~= 0 then
253-
-- unexpected range
254-
return nil
255-
end
277+
-- if choice.range.start.character ~= 0 then
278+
-- -- unexpected range
279+
-- return nil
280+
-- end
256281

257282
return choice
258283
end)
@@ -475,7 +500,7 @@ local function schedule(ctx)
475500
stop_timer()
476501
end
477502

478-
update_preview(ctx)
503+
-- update_preview(ctx)
479504
local bufnr = vim.api.nvim_get_current_buf()
480505
copilot._copilot_timer = vim.fn.timer_start(copilot.debounce, function(timer)
481506
logger.trace("suggestion schedule timer", bufnr)
@@ -487,6 +512,10 @@ function M.next()
487512
local ctx = get_ctx()
488513
logger.trace("suggestion next", ctx)
489514

515+
if ctx.accepted_partial then
516+
reset_ctx(ctx)
517+
end
518+
490519
-- no suggestion request yet
491520
if not ctx.first then
492521
logger.trace("suggestion next, no first request")
@@ -503,6 +532,10 @@ function M.prev()
503532
local ctx = get_ctx()
504533
logger.trace("suggestion prev", ctx)
505534

535+
if ctx.accepted_partial then
536+
reset_ctx(ctx)
537+
end
538+
506539
-- no suggestion request yet
507540
if not ctx.first then
508541
logger.trace("suggestion prev, no first request", ctx)
@@ -532,13 +565,17 @@ function M.accept(modifier)
532565
return
533566
end
534567

535-
cancel_inflight_requests(ctx)
536-
reset_ctx(ctx)
537-
538568
if type(modifier) == "function" then
539569
suggestion = modifier(suggestion)
540570
end
541571

572+
local accepted_partial = suggestion.partial_text and suggestion.partial_text ~= ""
573+
574+
if not accepted_partial then
575+
cancel_inflight_requests(ctx)
576+
reset_ctx(ctx)
577+
end
578+
542579
with_client(function(client)
543580
local ok, _ = pcall(function()
544581
api.notify_accepted(
@@ -552,34 +589,62 @@ function M.accept(modifier)
552589
end
553590
end)
554591

555-
clear_preview()
592+
local newText
593+
594+
if accepted_partial then
595+
newText = suggestion.partial_text
596+
ctx.accepted_partial = true
597+
ignore_next_cursor_moved = true
598+
else
599+
clear_preview()
600+
newText = suggestion.text
601+
end
556602

557-
local range, newText = suggestion.range, suggestion.text
603+
local range = suggestion.range
558604
local cursor = vim.api.nvim_win_get_cursor(0)
559605
local line, character = cursor[1] - 1, cursor[2]
560606
if range["end"].line == line and range["end"].character < character then
561607
range["end"].character = character
562608
end
563609

564-
-- Hack for 'autoindent', makes the indent persist. Check `:help 'autoindent'`.
565610
vim.schedule_wrap(function()
566611
-- Create an undo breakpoint
567612
vim.cmd("let &undolevels=&undolevels")
613+
-- Hack for 'autoindent', makes the indent persist. Check `:help 'autoindent'`.
568614
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Space><Left><Del>", true, false, true), "n", false)
569615
local bufnr = vim.api.nvim_get_current_buf()
616+
570617
local encoding = vim.api.nvim_get_option_value("fileencoding", { buf = bufnr }) ~= ""
571618
and vim.api.nvim_get_option_value("fileencoding", { buf = bufnr })
572619
or vim.api.nvim_get_option_value("encoding", { scope = "global" })
573-
vim.lsp.util.apply_text_edits({ { range = range, newText = newText } }, bufnr, encoding)
574620

575-
-- instead of calling <End>, go to the pos of the row after the last \n of inserted text
576-
-- local cursor_keys = string.rep("<Down>", #vim.split(newText, "\n", { plain = true }) - 1) .. "<End>"
577-
-- vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(cursor_keys, true, false, true), "n", false)
578621
local lines = vim.split(newText, "\n", { plain = true })
579-
local last_line = lines[#lines]
580-
local cursor_keys = string.rep("<Down>", #lines - 1)
622+
local lines_count = #lines
623+
local last_col = #lines[lines_count]
624+
625+
-- apply_text_edits will remove the last \n if the last line is empty,
626+
-- so we trick it by adding an extra one
627+
if last_col == 0 then
628+
newText = newText .. "\n"
629+
end
630+
631+
vim.lsp.util.apply_text_edits({ { range = range, newText = newText } }, bufnr, encoding)
632+
581633
-- Position cursor at the end of the last inserted line
582-
vim.api.nvim_win_set_cursor(0, { range["start"].line + #lines, #last_line })
634+
local new_cursor_line = range["start"].line + #lines
635+
vim.api.nvim_win_set_cursor(0, { new_cursor_line, last_col })
636+
637+
if accepted_partial then
638+
suggestion.partial_text = nil
639+
640+
for _ = 1, lines_count - 1 do
641+
suggestion.text = suggestion.text:sub(suggestion.text:find("\n") + 1)
642+
suggestion.displayText = suggestion.displayText:sub(suggestion.displayText:find("\n") + 1)
643+
end
644+
645+
update_ctx_suggestion_position(ctx.choice, new_cursor_line - 1, last_col, bufnr)
646+
update_preview(ctx)
647+
end
583648
end)()
584649
end
585650

@@ -592,29 +657,30 @@ function M.accept_word()
592657

593658
local _, char_idx = string.find(text, "%s*%p*[^%s%p]*%s*", character + 1)
594659
if char_idx then
595-
suggestion.text = string.sub(text, 1, char_idx)
596-
597-
range["end"].line = range["start"].line
660+
suggestion.partial_text = string.sub(text, 1, char_idx)
598661
range["end"].character = char_idx
599662
end
600663

664+
range["end"].line = range["start"].line
601665
return suggestion
602666
end)
603667
end
604668

605669
function M.accept_line()
606670
M.accept(function(suggestion)
607-
local text = suggestion.text
671+
local range, text = suggestion.range, suggestion.text
608672

609673
local cursor = vim.api.nvim_win_get_cursor(0)
610674
local _, character = cursor[1], cursor[2]
611675

612676
local next_char = string.sub(text, character + 1, character + 1)
613677
local _, char_idx = string.find(text, next_char == "\n" and "\n%s*[^\n]*\n%s*" or "\n%s*", character)
614678
if char_idx then
615-
suggestion.text = string.sub(text, 1, char_idx)
679+
suggestion.partial_text = string.sub(text, 1, char_idx)
680+
range["end"].character = char_idx
616681
end
617682

683+
range["end"].line = range["start"].line
618684
return suggestion
619685
end)
620686
end
@@ -659,6 +725,11 @@ local function on_buf_enter()
659725
end
660726

661727
local function on_cursor_moved_i()
728+
if ignore_next_cursor_moved then
729+
ignore_next_cursor_moved = false
730+
return
731+
end
732+
662733
local ctx = get_ctx()
663734
if copilot._copilot_timer or ctx.params or should_auto_trigger() then
664735
logger.trace("suggestion on cursor moved insert")

tests/child_helper.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function M.new_child_neovim(test_name)
2121
panel = "",
2222
suggestion = [[
2323
suggestion_notification = function(virt_text, _)
24-
if (#virt_text > 0) and (#virt_text[1] > 0) and (virt_text[1][1] == "89") then
24+
if (#virt_text > 0) and (#virt_text[1] > 0) then
2525
M.suggested = true
2626
end
2727
end,
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
--|---------|---------|---------|---------|
2+
01|# Numbers in a 3x3 grid, up to 63
3+
02|{
4+
03| 1,2,3
5+
04| 4,5,6
6+
05| 7,8,9
7+
06|}
8+
07|{
9+
08| 10,11,12
10+
09| 13,14,15
11+
10| 16,17,18
12+
11|}
13+
12|{
14+
13| 19,20,21
15+
14| 22,23,24
16+
15| 25,26,27
17+
16|}
18+
17|{
19+
18| 28,29,30
20+
19| 31,32,33
21+
20| 34,35,36
22+
21|}
23+
22|{
24+
23| 37,38,39
25+
24| 40,41,42
26+
25| 43,44,45
27+
26|}
28+
27|{
29+
28| 46,47,48
30+
29| 49,50,51
31+
30| 52,53,54
32+
31|}
33+
32|{
34+
33| 55,56,57
35+
34| 58,59,60
36+
35| 61,62,63
37+
36|}
38+
37|~
39+
38|~
40+
39|~
41+
40|~
42+
41|~
43+
42|~
44+
43|~
45+
44|~
46+
45|~
47+
46|~
48+
47|~
49+
48|~
50+
49|[No Name] [+] 36,2 All
51+
50|-- INSERT --
52+
53+
--|---------|---------|---------|---------|
54+
01|0000000000000000000000000000000000000000
55+
02|0000000000000000000000000000000000000000
56+
03|0000000000000000000000000000000000000000
57+
04|0000000000000000000000000000000000000000
58+
05|0000000000000000000000000000000000000000
59+
06|0000000000000000000000000000000000000000
60+
07|0000000000000000000000000000000000000000
61+
08|0000000000000000000000000000000000000000
62+
09|0000000000000000000000000000000000000000
63+
10|0000000000000000000000000000000000000000
64+
11|0000000000000000000000000000000000000000
65+
12|0000000000000000000000000000000000000000
66+
13|0000000000000000000000000000000000000000
67+
14|0000000000000000000000000000000000000000
68+
15|0000000000000000000000000000000000000000
69+
16|0000000000000000000000000000000000000000
70+
17|0000000000000000000000000000000000000000
71+
18|0000000000000000000000000000000000000000
72+
19|0000000000000000000000000000000000000000
73+
20|0000000000000000000000000000000000000000
74+
21|0000000000000000000000000000000000000000
75+
22|0000000000000000000000000000000000000000
76+
23|0000000000000000000000000000000000000000
77+
24|0000000000000000000000000000000000000000
78+
25|0000000000000000000000000000000000000000
79+
26|0000000000000000000000000000000000000000
80+
27|0000000000000000000000000000000000000000
81+
28|0000000000000000000000000000000000000000
82+
29|0000000000000000000000000000000000000000
83+
30|0000000000000000000000000000000000000000
84+
31|0000000000000000000000000000000000000000
85+
32|1000000000000000000000000000000000000000
86+
33|0000000000000000000000000000000000000000
87+
34|0000000000000000000000000000000000000000
88+
35|0000000000000000000000000000000000000000
89+
36|1000000000000000000000000000000000000000
90+
37|2222222222222222222222222222222222222222
91+
38|2222222222222222222222222222222222222222
92+
39|2222222222222222222222222222222222222222
93+
40|2222222222222222222222222222222222222222
94+
41|2222222222222222222222222222222222222222
95+
42|2222222222222222222222222222222222222222
96+
43|2222222222222222222222222222222222222222
97+
44|2222222222222222222222222222222222222222
98+
45|2222222222222222222222222222222222222222
99+
46|2222222222222222222222222222222222222222
100+
47|2222222222222222222222222222222222222222
101+
48|2222222222222222222222222222222222222222
102+
49|3333333333333333333333333333333333333333
103+
50|4444444444445555555555555555555555555555

0 commit comments

Comments
 (0)