Skip to content

Commit bbdb662

Browse files
authored
feat: debounce NES autocommands (#2)
* feat: debounce NES autocommands * refactor: update NES API usage Remove unused inline completion code and refactor NES functions to remove redundant opts and client parameters. Clean up autocommands and key mappings to use the streamlined NES API. * docs: add usage guide and remove fixed keymap Add a usage section to README.md with a Neovim configuration example. * feat(nes): navigate to suggestion end Update the apply_pending_nes function to create a new jump_loc that navigates to the end of the text range. This change displays the document at the correct position before applying the inline edit. * feat: update keymap & inline edit flow - Add Blink integration section to README.md. - Update inline edit handling in nes to return boolean and schedule mode switching for integration with blink * fix(util): use final function args in debounce Remove redundant local variable assignment and update debounce to capture and pass the final arguments. Add tests to confirm final parameters are used. * ci: make timings more forgiving * refactor(tests): update debounce tests to use lua_func Refactor debounce tests in test_util.lua to use child.lua_func for running tests. * refactor(debounce): folke debounce https://github.com/folke/noice.nvim/blob/0427460c2d7f673ad60eb02b35f5e9926cf67c59/lua/noice/util/init.lua#L104 * feat: configurable debounce for Copilot NES Allow configuration of the debounce interval for Copilot NES by using vim.g.copilot_nes_debounce in the initialization. If not set, a default of 500ms is used. * refactor: swap vim.loop to vim.uv * refactor(lsp): upvalue for debounce
1 parent 69d0934 commit bbdb662

File tree

8 files changed

+229
-39
lines changed

8 files changed

+229
-39
lines changed

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,60 @@
1717
- Sign In Flow
1818
- Status Notification
1919

20+
## Usage
21+
22+
To use the plugin, add the following to your Neovim configuration:
23+
24+
```lua
25+
return {
26+
"copilotlsp-nvim/copilot-lsp",
27+
init = function()
28+
vim.g.copilot_nes_debounce = 500
29+
vim.lsp.enable("copilot")
30+
vim.keymap.set("n", "<tab>", function()
31+
require("copilot-lsp.nes").apply_pending_nes()
32+
end)
33+
end,
34+
}
35+
```
36+
37+
### Blink Integration
38+
39+
```lua
40+
return {
41+
keymap = {
42+
preset = "super-tab",
43+
["<Tab>"] = {
44+
function(cmp)
45+
if vim.b[vim.api.nvim_get_current_buf()].nes_state then
46+
cmp.hide()
47+
return require("copilot-lsp.nes").apply_pending_nes()
48+
end
49+
if cmp.snippet_active() then
50+
return cmp.accept()
51+
else
52+
return cmp.select_and_accept()
53+
end
54+
end,
55+
"snippet_forward",
56+
"fallback",
57+
},
58+
},
59+
}
60+
```
61+
62+
It can also be combined with [fang2hou/blink-copilot](https://github.com/fang2hou/blink-copilot) to get inline completions.
63+
Just add the completion source to your Blink configuration and it will integrate
64+
2065
# Requirements
2166

2267
- Copilot LSP installed via Mason or system and on PATH
2368

2469
### Screenshots
2570

2671
#### NES
72+
2773
![JS Correction](https://github.com/user-attachments/assets/8941f8f9-7d1b-4521-b8e9-f1dcd12d31e9)
2874
![Go Insertion](https://github.com/user-attachments/assets/2c0c4ad9-873b-4860-9eff-ecdb76007234)
2975

30-
https://github.com/user-attachments/assets/1d5bed4a-fd0a-491f-91f3-a3335cc28682
76+
<https://github.com/user-attachments/assets/1d5bed4a-fd0a-491f-91f3-a3335cc28682>

lsp/copilot.lua

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,33 +25,34 @@ return {
2525
vim.api.nvim_set_hl(0, "NesDelete", { link = "DiffDelete", default = true })
2626
vim.api.nvim_set_hl(0, "NesApply", { link = "DiffText", default = true })
2727

28-
local nes = require("copilot-lsp.nes")
29-
local inline_completion = require("copilot-lsp.completion")
28+
local au = vim.api.nvim_create_augroup("copilot-language-server", { clear = true })
29+
--NOTE: Inline Completions
30+
--TODO: We dont currently use this code path, so comment for now until a UI is built
31+
-- vim.api.nvim_create_autocmd("TextChangedI", {
32+
-- callback = function()
33+
-- inline_completion.request_inline_completion(2)
34+
-- end,
35+
-- group = au,
36+
-- })
3037

3138
-- TODO: make this configurable for key maps, or just expose commands to map in config
32-
vim.keymap.set("i", "<c-i>", function()
33-
inline_completion.request_inline_completion(1)
34-
end)
35-
36-
--TODO: This should go on an auto command
37-
--I would like to debounce this, and call it automatically
38-
--but we need to work on the clean up logic/tracking for the UI
39-
vim.keymap.set("n", "<leader>x", function()
40-
nes.request_nes(client)
41-
end)
39+
-- vim.keymap.set("i", "<c-i>", function()
40+
-- inline_completion.request_inline_completion(1)
41+
-- end)
4242

43-
vim.keymap.set("n", "<leader>xa", function()
44-
nes.apply_pending_nes(nil, { trigger = true, jump = true }, client)
45-
end)
46-
47-
local au = vim.api.nvim_create_augroup("copilot-language-server", { clear = true })
48-
vim.api.nvim_create_autocmd("TextChangedI", {
43+
--NOTE: NES Completions
44+
local debounced_request = require("copilot-lsp.util").debounce(
45+
require("copilot-lsp.nes").request_nes,
46+
vim.g.copilot_nes_debounce or 500
47+
)
48+
vim.api.nvim_create_autocmd({ "TextChangedI", "TextChanged" }, {
4949
callback = function()
50-
inline_completion.request_inline_completion(2)
50+
debounced_request(client)
5151
end,
5252
group = au,
5353
})
5454

55+
--NOTE: didFocus
5556
vim.api.nvim_create_autocmd("BufEnter", {
5657
callback = function()
5758
local td_params = vim.lsp.util.make_text_document_params()

lua/copilot-lsp/nes/init.lua

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ local nes_ns = vim.api.nvim_create_namespace("copilot-nes")
1010
---@param result copilotlsp.copilotInlineEditResponse
1111
local function handle_nes_response(err, result)
1212
if err then
13-
vim.notify(err.message)
13+
-- vim.notify(err.message)
1414
return
1515
end
1616
for _, edit in ipairs(result.edits) do
@@ -31,25 +31,36 @@ function M.request_nes(copilot_lss)
3131
end
3232

3333
---@param bufnr? integer
34-
---@param opts? nes.Apply.Opts
35-
---@param client vim.lsp.Client
36-
function M.apply_pending_nes(bufnr, opts, client)
37-
opts = opts or {}
38-
34+
---@return boolean
35+
function M.apply_pending_nes(bufnr)
3936
bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf()
4037

4138
---@type copilotlsp.InlineEdit
4239
local state = vim.b[bufnr].nes_state
4340
if not state then
44-
return
45-
end
46-
utils.apply_inline_edit(state)
47-
nes_ui.clear_suggestion(bufnr, nes_ns)
48-
if opts.trigger then
49-
vim.schedule(function()
50-
M.request_nes(client)
51-
end)
41+
return false
5242
end
43+
vim.schedule(function()
44+
local prev_mode = vim.api.nvim_get_mode().mode
45+
if prev_mode == "i" then
46+
vim.cmd("stopinsert!")
47+
end
48+
---@type lsp.Location
49+
local jump_loc = {
50+
uri = state.textDocument.uri,
51+
range = {
52+
start = state.range["end"],
53+
["end"] = state.range["end"],
54+
},
55+
}
56+
vim.lsp.util.show_document(jump_loc, "utf-16", { focus = true })
57+
utils.apply_inline_edit(state)
58+
nes_ui.clear_suggestion(bufnr, nes_ns)
59+
if prev_mode == "i" then
60+
vim.cmd("startinsert")
61+
end
62+
end)
63+
return true
5364
end
5465

5566
return M

lua/copilot-lsp/nes/state.lua

Whitespace-only changes.

lua/copilot-lsp/nes/ui.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,13 @@ end
7474
---@param edits copilotlsp.InlineEdit[]
7575
---@param ns_id integer
7676
function M._display_next_suggestion(edits, ns_id)
77+
local state = vim.b[vim.api.nvim_get_current_buf()].nes_state
78+
if state then
79+
M.clear_suggestion(vim.api.nvim_get_current_buf(), ns_id)
80+
end
81+
7782
if not edits or #edits == 0 then
78-
vim.notify("No suggestion available", vim.log.levels.INFO)
83+
-- vim.notify("No suggestion available", vim.log.levels.INFO)
7984
return
8085
end
8186
local bufnr = vim.uri_to_bufnr(edits[1].textDocument.uri)

lua/copilot-lsp/types.lua

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@
1212
---@class nes.EditSuggestionUI
1313
---@field preview_winnr? integer
1414

15-
---@class nes.Apply.Opts
16-
---@field jump? boolean | { hl_timeout: integer? } auto jump to the end of the new edit
17-
---@field trigger? boolean auto trigger the next edit suggestion
18-
1915
---@class nes.DeleteExtmark
2016
--- Holds row information for delete highlight extmark.
2117
---@field row number

lua/copilot-lsp/util.lua

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,19 @@ function M.apply_inline_edit(edit)
77
vim.lsp.util.apply_text_edits({ edit }, bufnr, "utf-16")
88
end
99

10+
---Debounces calls to a function, and ensures it only runs once per delay
11+
---even if called repeatedly.
12+
---@param fn fun(...: any)
13+
---@param delay integer
14+
function M.debounce(fn, delay)
15+
local timer = vim.uv.new_timer()
16+
return function(...)
17+
local argv = vim.F.pack_len(...)
18+
timer:start(delay, 0, function()
19+
timer:stop()
20+
vim.schedule_wrap(fn)(vim.F.unpack_len(argv))
21+
end)
22+
end
23+
end
24+
1025
return M

tests/test_util.lua

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
local eq = MiniTest.expect.equality
2+
3+
local child = MiniTest.new_child_neovim()
4+
5+
local T = MiniTest.new_set()
6+
7+
T["debounce"] = MiniTest.new_set({
8+
hooks = {
9+
pre_case = function()
10+
child.restart({ "-u", "scripts/minimal_init.lua" })
11+
end,
12+
post_once = child.stop,
13+
},
14+
})
15+
T["debounce"]["debounces calls to a function"] = function()
16+
child.lua_func(function()
17+
_G.called = 0
18+
local fn = function()
19+
_G.called = _G.called + 1
20+
end
21+
22+
local debounced_fn = require("copilot-lsp.util").debounce(fn, 500)
23+
debounced_fn()
24+
end)
25+
26+
local called = child.lua_func(function()
27+
return _G.called
28+
end)
29+
eq(called, 0)
30+
31+
vim.uv.sleep(100)
32+
called = child.lua_func(function()
33+
return _G.called
34+
end)
35+
eq(called, 0)
36+
vim.uv.sleep(500)
37+
called = child.lua_func(function()
38+
return _G.called
39+
end)
40+
eq(called, 1)
41+
end
42+
T["debounce"]["function is called with final calls params"] = function()
43+
child.lua_func(function()
44+
_G.called = 0
45+
local fn = function(a)
46+
_G.called = a
47+
end
48+
49+
local debounced_fn = require("copilot-lsp.util").debounce(fn, 500)
50+
debounced_fn(1)
51+
debounced_fn(2)
52+
debounced_fn(3)
53+
end)
54+
55+
local called = child.lua("return _G.called")
56+
eq(called, 0)
57+
58+
vim.uv.sleep(100)
59+
called = child.lua("return _G.called")
60+
eq(called, 0)
61+
vim.uv.sleep(100)
62+
called = child.lua("return _G.called")
63+
eq(called, 0)
64+
vim.uv.sleep(500)
65+
called = child.lua("return _G.called")
66+
eq(called, 3)
67+
end
68+
T["debounce"]["function is only called once"] = function()
69+
child.lua_func(function()
70+
_G.called = {}
71+
local fn = function(a)
72+
table.insert(_G.called, a)
73+
end
74+
75+
_G.debounced_fn = require("copilot-lsp.util").debounce(fn, 250)
76+
end)
77+
78+
child.lua_func(function()
79+
_G.debounced_fn(0)
80+
_G.debounced_fn(1)
81+
_G.debounced_fn(2)
82+
_G.debounced_fn(3)
83+
_G.debounced_fn(4)
84+
end)
85+
86+
local called = child.lua_func(function()
87+
return _G.called
88+
end)
89+
eq(#called, 0)
90+
91+
vim.uv.sleep(500)
92+
93+
called = child.lua_func(function()
94+
return _G.called
95+
end)
96+
eq(#called, 1)
97+
98+
child.lua_func(function()
99+
_G.debounced_fn(5)
100+
_G.debounced_fn(6)
101+
_G.debounced_fn(7)
102+
_G.debounced_fn(8)
103+
_G.debounced_fn(9)
104+
end)
105+
106+
vim.uv.sleep(500)
107+
108+
called = child.lua_func(function()
109+
return _G.called
110+
end)
111+
eq(#called, 2)
112+
113+
eq(called, { 4, 9 })
114+
end
115+
116+
return T

0 commit comments

Comments
 (0)