Skip to content

Commit fc82256

Browse files
committed
feat: next edit suggestion (nes)
1 parent 8e52d47 commit fc82256

18 files changed

+572
-107
lines changed

README.md

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# copilot.lua
22

33
This plugin is the pure lua replacement for [github/copilot.vim](https://github.com/github/copilot.vim).
4+
A huge thank you to @tris203 for the code behind the nes functionality ([copilot-lsp](https://github.com/copilotlsp-nvim/copilot-lsp)).
45

56
<details>
67
<summary>Motivation behind `copilot.lua`</summary>
@@ -48,7 +49,11 @@ Install the plugin with your preferred plugin manager.
4849
For example, with [packer.nvim](https://github.com/wbthomason/packer.nvim):
4950

5051
```lua
51-
use { "zbirenbaum/copilot.lua" }
52+
use { "zbirenbaum/copilot.lua"
53+
requires = {
54+
"copilotlsp-nvim/copilot-lsp", -- (optional) for NES functionality
55+
},
56+
}
5257
```
5358

5459
### Authentication
@@ -93,6 +98,9 @@ For example:
9398
```lua
9499
use {
95100
"zbirenbaum/copilot.lua",
101+
requires = {
102+
"copilotlsp-nvim/copilot-lsp", -- (optional) for NES functionality
103+
},
96104
cmd = "Copilot",
97105
event = "InsertEnter",
98106
config = function()
@@ -136,16 +144,14 @@ require('copilot').setup({
136144
dismiss = "<C-]>",
137145
},
138146
},
139-
filetypes = {
140-
yaml = false,
141-
markdown = false,
142-
help = false,
143-
gitcommit = false,
144-
gitrebase = false,
145-
hgcommit = false,
146-
svn = false,
147-
cvs = false,
148-
["."] = false,
147+
nes = {
148+
enabled = false, -- requires copilot-lsp as a dependency
149+
auto_trigger = false,
150+
keymap = {
151+
accept_and_goto = false,
152+
accept = false,
153+
dismiss = false,
154+
},
149155
},
150156
auth_provider_url = nil, -- URL to authentication provider, if not "https://github.com/"
151157
logger = {
@@ -270,6 +276,43 @@ require("copilot.suggestion").toggle_auto_trigger()
270276
```
271277
These can also be accessed through the `:Copilot suggestion <function>` command (eg. `:Copilot suggestion accept`).
272278

279+
### nes (next edit suggestion)
280+
281+
>[!WARNING]
282+
> This feature is still experimental and may not work as expected in all scenarios, please report any issues you encounter.
283+
284+
When `enabled` is `true`, copilot will provide suggestions based on the next edit you are likely to make, through [copilot-lsp](https://github.com/copilotlsp-nvim/copilot-lsp).
285+
If there is no suggestion, the keymaps will pass through the original keymap.
286+
287+
`copilot-lsp` has a few configurations built-in as well, for additional configurations, please refer to the [copilot-lsp documentation](https://github.com/copilotlsp-nvim/copilot-lsp/blob/main/README.md).
288+
These configurations should be set in the `init` function of the `copilot-lsp` dependency.
289+
290+
```lua
291+
use {
292+
"zbirenbaum/copilot.lua",
293+
requires = {
294+
"copilotlsp-nvim/copilot-lsp",
295+
init = function()
296+
vim.g.copilot_nes_debounce = 500
297+
end,
298+
},
299+
cmd = "Copilot",
300+
event = "InsertEnter",
301+
config = function()
302+
require("copilot").setup({
303+
nes = {
304+
enabled = true,
305+
keymap = {
306+
accept_and_goto = "<leader>p",
307+
accept = false,
308+
dismiss = "<Esc>",
309+
},
310+
},
311+
})
312+
end,
313+
}
314+
```
315+
273316
### filetypes
274317

275318
Specify filetypes for attaching copilot.

lua/copilot/client/config.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ function M.prepare_client_config(overrides, client)
122122
if token_env_set then
123123
require("copilot.auth").signin()
124124
end
125+
126+
require("copilot.nes").setup(lsp_client)
125127
end)
126128
end,
127129
on_exit = function(code, _, client_id)

lua/copilot/config/init.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ local logger = require("copilot.logger")
55
---@field suggestion SuggestionConfig
66
---@field logger LoggerConfig
77
---@field server ServerConfig
8+
---@field nes NesConfig
89
---@field filetypes table<string, boolean> Filetypes to enable Copilot for
910
---@field auth_provider_url string|nil URL for the authentication provider
1011
---@field workspace_folders string[] Workspace folders to enable Copilot for
@@ -23,6 +24,7 @@ local M = {
2324
suggestion = require("copilot.config.suggestion").default,
2425
logger = require("copilot.config.logger").default,
2526
server = require("copilot.config.server").default,
27+
nes = require("copilot.config.nes").default,
2628
root_dir = require("copilot.config.root_dir").default,
2729
should_attach = require("copilot.config.should_attach").default,
2830
filetypes = {},
@@ -59,6 +61,7 @@ end
5961
function M.validate(config)
6062
vim.validate("panel", config.panel, "table")
6163
vim.validate("suggestion", config.suggestion, "table")
64+
vim.validate("nes", config.nes, "table")
6265
vim.validate("logger", config.logger, "table")
6366
vim.validate("server", config.server, "table")
6467
vim.validate("filetypes", config.filetypes, "table")
@@ -76,6 +79,7 @@ function M.validate(config)
7679
require("copilot.config.server").validate(config.server)
7780
require("copilot.config.root_dir").validate(config.root_dir)
7881
require("copilot.config.should_attach").validate(config.should_attach)
82+
require("copilot.config.nes").validate(config.nes)
7983
end
8084

8185
return M

lua/copilot/config/nes.lua

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
local logger = require("copilot.logger")
2+
local nes_api = require("copilot.nes.api")
3+
---@class NesKeymap
4+
---@field accept_and_goto string|false Keymap to accept the suggestion and go to the end of the suggestion
5+
---@field accept string|false Keymap to accept the suggestion
6+
---@field dismiss string|false Keymap to dismiss the suggestion
7+
8+
---@class NesConfig
9+
---@field enabled boolean Whether to enable nes (next edit suggestions)
10+
---@field auto_trigger boolean Whether to automatically trigger next edit suggestions
11+
---@field keymap NesKeymap Keymaps for nes actions
12+
13+
local M = {
14+
---@type NesConfig
15+
default = {
16+
enabled = false,
17+
auto_trigger = false,
18+
keymap = {
19+
accept_and_goto = false,
20+
accept = false,
21+
dismiss = false,
22+
},
23+
},
24+
}
25+
26+
---@type NesConfig
27+
M.config = vim.deepcopy(M.default)
28+
29+
---@param opts? NesConfig
30+
function M.setup(opts)
31+
opts = opts or {}
32+
M.config = vim.tbl_deep_extend("force", M.default, opts)
33+
end
34+
35+
---@param config NesConfig
36+
function M.validate(config)
37+
vim.validate("enabled", config.enabled, "boolean")
38+
vim.validate("auto_trigger", config.auto_trigger, "boolean")
39+
vim.validate("keymap", config.keymap, "table")
40+
vim.validate("keymap.accept_and_goto", config.keymap.accept_and_goto, { "string", "boolean" })
41+
vim.validate("keymap.accept", config.keymap.accept, { "string", "boolean" })
42+
vim.validate("keymap.dismiss", config.keymap.dismiss, { "string", "boolean" })
43+
44+
if config.enabled then
45+
local has_nes, _ = pcall(function()
46+
nes_api.test()
47+
end)
48+
49+
if not has_nes then
50+
logger.error(
51+
"copilot-lsp is not available, disabling nes.\nPlease refer to the documentation and ensure it is installed."
52+
)
53+
config.enabled = false
54+
end
55+
end
56+
end
57+
58+
return M

lua/copilot/highlight.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
local logger = require("copilot.logger")
2+
local config = require("copilot.config")
3+
local nes_api = require("copilot.nes.api")
4+
15
local M = {
26
group = {
37
CopilotAnnotation = "CopilotAnnotation",
@@ -19,6 +23,16 @@ function M.setup()
1923
vim.api.nvim_set_hl(0, from_group, { link = to_group })
2024
end
2125
end
26+
27+
if config.nes.enabled then
28+
local ok, err = pcall(function()
29+
nes_api.set_hl()
30+
end)
31+
32+
if not ok then
33+
logger.error("Error setting copilot-lsp highlights: ", err)
34+
end
35+
end
2236
end)
2337
end
2438

lua/copilot/keymaps/init.lua

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
local logger = require("copilot.logger")
2+
local config = require("copilot.config")
3+
4+
local M = {}
5+
6+
local previous_keymaps = {}
7+
8+
---@param mode string
9+
---@param key string
10+
---@param action function
11+
---@param desc string
12+
function M.register_keymap(mode, key, action, desc)
13+
if not mode or not key or not action then
14+
return
15+
end
16+
17+
vim.keymap.set(mode, key, function()
18+
action()
19+
end, {
20+
desc = desc,
21+
silent = true,
22+
})
23+
end
24+
25+
---@param mode string
26+
---@param key string
27+
---@param action function: boolean
28+
---@param desc string
29+
function M.register_keymap_with_passthrough(mode, key, action, desc)
30+
if not mode or not key or not action then
31+
return
32+
end
33+
34+
local keymap_key = mode .. ":" .. key
35+
-- Save any existing mapping for this key
36+
local existing = vim.fn.maparg(key, mode, false, true)
37+
if existing and existing.rhs and existing.rhs ~= "" then
38+
previous_keymaps[keymap_key] = existing.rhs
39+
else
40+
previous_keymaps[keymap_key] = nil
41+
end
42+
43+
vim.keymap.set(mode, key, function()
44+
local action_ran = action()
45+
if not action_ran then
46+
-- If there was a previous mapping, execute it
47+
local prev = previous_keymaps[keymap_key]
48+
if prev then
49+
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(prev, true, false, true), mode, true)
50+
end
51+
end
52+
end, {
53+
desc = desc,
54+
silent = true,
55+
})
56+
end
57+
58+
---@param mode string
59+
---@param key string|false
60+
function M.unset_keymap_if_exists(mode, key)
61+
if not key then
62+
return
63+
end
64+
65+
local ok, err = pcall(vim.api.nvim_del_keymap, mode, key)
66+
67+
if not ok then
68+
local suggestion_keymaps = config.suggestion.keymap or {}
69+
local panel_keymaps = config.panel.keymap or {}
70+
local found = false
71+
72+
for _, tbl in ipairs({ suggestion_keymaps, panel_keymaps }) do
73+
for _, v in pairs(tbl) do
74+
if v == key then
75+
if found then
76+
logger.error("Keymap " .. key .. " is used for two different actions, please review your configuration.")
77+
return
78+
else
79+
found = true
80+
end
81+
end
82+
end
83+
end
84+
85+
logger.error("Could not unset keymap for " .. mode .. " " .. key .. ": " .. err)
86+
end
87+
end
88+
89+
return M

lua/copilot/logger/init.lua

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,20 @@ function M.handle_lsp_progress(_, result, _)
171171
M.trace(string.format("LSP progress - token %s", result.token), result.value)
172172
end
173173

174+
-- Known noisy errors that we do not want to show as they seem to be out of our control
175+
---@param msg string
176+
---@return boolean
177+
local function force_log_to_trace(msg)
178+
if
179+
msg:match(".*Request textDocument/copilotInlineEdit: AbortError: The operation was aborted.*")
180+
or msg:match(".*AsyncCompletionManager.*Request errored with AbortError: The operation was aborted.*")
181+
then
182+
return true
183+
end
184+
185+
return false
186+
end
187+
174188
function M.handle_log_lsp_messages(_, result, _)
175189
if not result then
176190
return
@@ -179,7 +193,7 @@ function M.handle_log_lsp_messages(_, result, _)
179193
local message = string.format("LSP message: %s", result.message)
180194
local message_type = result.type --[[@as integer]]
181195

182-
if message_type == 1 then
196+
if message_type == 1 and not force_log_to_trace(message) then
183197
M.error(message)
184198
elseif message_type == 2 then
185199
M.warn(message)

lua/copilot/nes/api.lua

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-- Abstraction to the copilot-lsp module
2+
3+
local M = {}
4+
5+
function M.nes_set_auto_trigger(value)
6+
local config = require("copilot-lsp.config")
7+
config.config.require("copilot-lsp.nes").auto_trigger = value
8+
end
9+
10+
function M.nes_lsp_on_init(client, au)
11+
require("copilot-lsp.nes").lsp_on_init(client, au)
12+
end
13+
14+
function M.set_hl()
15+
local util = require("copilot-lsp.util")
16+
util.set_hl()
17+
end
18+
19+
---@param bufnr? integer
20+
---@return boolean --if the cursor walked
21+
function M.nes_walk_cursor_start_edit(bufnr)
22+
return require("copilot-lsp.nes").walk_cursor_start_edit(bufnr)
23+
end
24+
25+
---@param bufnr? integer
26+
---@return boolean --if the cursor walked
27+
function M.nes_walk_cursor_end_edit(bufnr)
28+
return require("copilot-lsp.nes").walk_cursor_end_edit(bufnr)
29+
end
30+
31+
---@param bufnr? integer
32+
---@return boolean --if the nes was applied
33+
function M.nes_apply_pending_nes(bufnr)
34+
return require("copilot-lsp.nes").apply_pending_nes(bufnr)
35+
end
36+
37+
---@return boolean -- true if a suggestion was cleared, false if no suggestion existed
38+
function M.nes_clear()
39+
return require("copilot-lsp.nes").clear()
40+
end
41+
42+
function M.test()
43+
require("copilot-lsp.nes")
44+
end
45+
46+
return M

0 commit comments

Comments
 (0)