Skip to content

Commit 9d07da5

Browse files
committed
feat: add automatic chat autocompletion
Adds new config option `chat_autocomplete` (enabled by default) that will trigger completion automatically when typing trigger characters (@, /, #, $). This replaces the nvim-cmp integration which is now deprecated in favor of this new solution. The automatic completion is debounced to avoid too many triggers and uses the same completion logic as before, just moved to separate function. Signed-off-by: Tomas Slusny <slusnucky@gmail.com>
1 parent 72595b8 commit 9d07da5

File tree

5 files changed

+104
-129
lines changed

5 files changed

+104
-129
lines changed

README.md

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ Also see [here](/lua/CopilotChat/config.lua):
276276
error_header = '## Error ', -- Header to use for errors
277277
separator = '───', -- Separator to use in chat
278278

279+
chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger)
279280
show_folds = true, -- Shows folds for sections in chat
280281
show_help = true, -- Shows help message as virtual lines when waiting for user input
281282
auto_follow_cursor = true, -- Auto-follow cursor in chat
@@ -575,30 +576,6 @@ Requires [fzf-lua](https://github.com/ibhagwan/fzf-lua) plugin to be installed.
575576

576577
</details>
577578

578-
<details>
579-
<summary>nvim-cmp integration</summary>
580-
581-
Requires [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) plugin to be installed (and properly configured).
582-
583-
```lua
584-
-- Registers copilot-chat source and enables it for copilot-chat filetype (so copilot chat window)
585-
require("CopilotChat.integrations.cmp").setup()
586-
587-
-- You might also want to disable default <tab> complete mapping for copilot chat when doing this
588-
require('CopilotChat').setup({
589-
mappings = {
590-
complete = {
591-
insert = '',
592-
},
593-
},
594-
-- rest of your config
595-
})
596-
```
597-
598-
![image](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/063fc99f-a4b2-4187-a065-0fdd287ebee2)
599-
600-
</details>
601-
602579
<details>
603580
<summary>render-markdown integration</summary>
604581

lua/CopilotChat/config.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ local select = require('CopilotChat.select')
8383
---@field answer_header string?
8484
---@field error_header string?
8585
---@field separator string?
86+
---@field chat_autocomplete boolean?
8687
---@field show_folds boolean?
8788
---@field show_help boolean?
8889
---@field auto_follow_cursor boolean?
@@ -114,6 +115,7 @@ return {
114115
error_header = '## Error ', -- Header to use for errors
115116
separator = '───', -- Separator to use in chat
116117

118+
chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger)
117119
show_folds = true, -- Shows folds for sections in chat
118120
show_help = true, -- Shows help message as virtual lines when waiting for user input
119121
auto_follow_cursor = true, -- Auto-follow cursor in chat

lua/CopilotChat/init.lua

Lines changed: 72 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,6 @@ local state = {
4444
help = nil,
4545
}
4646

47-
local function blend_color_with_neovim_bg(color_name, blend)
48-
local color_int = vim.api.nvim_get_hl(0, { name = color_name }).fg
49-
local bg_int = vim.api.nvim_get_hl(0, { name = 'Normal' }).bg
50-
51-
if not color_int or not bg_int then
52-
return
53-
end
54-
55-
local color = { (color_int / 65536) % 256, (color_int / 256) % 256, color_int % 256 }
56-
local bg = { (bg_int / 65536) % 256, (bg_int / 256) % 256, bg_int % 256 }
57-
local r = math.floor((color[1] * blend + bg[1] * (100 - blend)) / 100)
58-
local g = math.floor((color[2] * blend + bg[2] * (100 - blend)) / 100)
59-
local b = math.floor((color[3] * blend + bg[3] * (100 - blend)) / 100)
60-
return string.format('#%02x%02x%02x', r, g, b)
61-
end
62-
6347
local function find_lines_between_separator(
6448
lines,
6549
current_line,
@@ -260,6 +244,47 @@ local function key_to_info(name, key, surround)
260244
return out
261245
end
262246

247+
local function trigger_complete()
248+
local info = M.complete_info()
249+
local bufnr = vim.api.nvim_get_current_buf()
250+
local line = vim.api.nvim_get_current_line()
251+
local cursor = vim.api.nvim_win_get_cursor(0)
252+
local row = cursor[1]
253+
local col = cursor[2]
254+
if col == 0 or #line == 0 then
255+
return
256+
end
257+
258+
local prefix, cmp_start = unpack(vim.fn.matchstrpos(line:sub(1, col), info.pattern))
259+
if not prefix then
260+
return
261+
end
262+
263+
if vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then
264+
local found_context = M.config.contexts[prefix:sub(2, -2)]
265+
if found_context and found_context.input then
266+
found_context.input(function(value)
267+
if not value then
268+
return
269+
end
270+
271+
vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { tostring(value) })
272+
end)
273+
end
274+
275+
return
276+
end
277+
278+
M.complete_items(function(items)
279+
vim.fn.complete(
280+
cmp_start + 1,
281+
vim.tbl_filter(function(item)
282+
return vim.startswith(item.word:lower(), prefix:lower())
283+
end, items)
284+
)
285+
end)
286+
end
287+
263288
--- Get the completion info for the chat window, for use with custom completion providers
264289
---@return table
265290
function M.complete_info()
@@ -275,8 +300,8 @@ function M.complete_items(callback)
275300
async.run(function()
276301
local models = state.copilot:list_models()
277302
local agents = state.copilot:list_agents()
278-
local items = {}
279303
local prompts_to_use = M.prompts()
304+
local items = {}
280305

281306
for name, prompt in pairs(prompts_to_use) do
282307
items[#items + 1] = {
@@ -323,6 +348,10 @@ function M.complete_items(callback)
323348
}
324349
end
325350

351+
table.sort(items, function(a, b)
352+
return a.kind < b.kind
353+
end)
354+
326355
vim.schedule(function()
327356
callback(items)
328357
end)
@@ -784,9 +813,17 @@ function M.setup(config)
784813
end
785814

786815
local hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights')
787-
vim.api.nvim_set_hl(hl_ns, '@diff.plus', { bg = blend_color_with_neovim_bg('DiffAdd', 20) })
788-
vim.api.nvim_set_hl(hl_ns, '@diff.minus', { bg = blend_color_with_neovim_bg('DiffDelete', 20) })
789-
vim.api.nvim_set_hl(hl_ns, '@diff.delta', { bg = blend_color_with_neovim_bg('DiffChange', 20) })
816+
vim.api.nvim_set_hl(hl_ns, '@diff.plus', { bg = utils.blend_color_with_neovim_bg('DiffAdd', 20) })
817+
vim.api.nvim_set_hl(
818+
hl_ns,
819+
'@diff.minus',
820+
{ bg = utils.blend_color_with_neovim_bg('DiffDelete', 20) }
821+
)
822+
vim.api.nvim_set_hl(
823+
hl_ns,
824+
'@diff.delta',
825+
{ bg = utils.blend_color_with_neovim_bg('DiffChange', 20) }
826+
)
790827
vim.api.nvim_set_hl(0, 'CopilotChatSpinner', { link = 'CursorColumn', default = true })
791828
vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true })
792829
vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true })
@@ -906,46 +943,23 @@ function M.setup(config)
906943

907944
map_key(M.config.mappings.reset, bufnr, M.reset)
908945
map_key(M.config.mappings.close, bufnr, M.close)
946+
map_key(M.config.mappings.complete, bufnr, trigger_complete)
909947

910-
map_key(M.config.mappings.complete, bufnr, function()
911-
local info = M.complete_info()
912-
local line = vim.api.nvim_get_current_line()
913-
local cursor = vim.api.nvim_win_get_cursor(0)
914-
local row = cursor[1]
915-
local col = cursor[2]
916-
if col == 0 or #line == 0 then
917-
return
918-
end
919-
920-
local prefix, cmp_start = unpack(vim.fn.matchstrpos(line:sub(1, col), info.pattern))
921-
if not prefix then
922-
return
923-
end
924-
925-
if vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then
926-
local found_context = M.config.contexts[prefix:sub(2, -2)]
927-
if found_context and found_context.input then
928-
found_context.input(function(value)
929-
if not value then
930-
return
931-
end
932-
933-
vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { tostring(value) })
934-
end)
935-
end
936-
937-
return
938-
end
948+
if M.config.chat_autocomplete then
949+
vim.api.nvim_create_autocmd('TextChangedI', {
950+
buffer = bufnr,
951+
callback = function()
952+
local line = vim.api.nvim_get_current_line()
953+
local cursor = vim.api.nvim_win_get_cursor(0)
954+
local col = cursor[2]
955+
local char = line:sub(col, col)
939956

940-
M.complete_items(function(items)
941-
vim.fn.complete(
942-
cmp_start + 1,
943-
vim.tbl_filter(function(item)
944-
return vim.startswith(item.word:lower(), prefix:lower())
945-
end, items)
946-
)
947-
end)
948-
end)
957+
if vim.tbl_contains(M.complete_info().triggers, char) then
958+
utils.debounce(trigger_complete, 100)
959+
end
960+
end,
961+
})
962+
end
949963

950964
map_key(M.config.mappings.submit_prompt, bufnr, function()
951965
local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)

lua/CopilotChat/integrations/cmp.lua

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,9 @@
1-
local cmp = require('cmp')
2-
local chat = require('CopilotChat')
3-
4-
local Source = {}
5-
6-
function Source:get_trigger_characters()
7-
return chat.complete_info().triggers
8-
end
9-
10-
function Source:get_keyword_pattern()
11-
return chat.complete_info().pattern
12-
end
13-
14-
function Source:complete(params, callback)
15-
chat.complete_items(function(items)
16-
items = vim.tbl_map(function(item)
17-
return {
18-
label = item.word,
19-
kind = cmp.lsp.CompletionItemKind.Keyword,
20-
}
21-
end, items)
22-
23-
local prefix = string.lower(params.context.cursor_before_line:sub(params.offset))
24-
25-
callback({
26-
items = vim.tbl_filter(function(item)
27-
return vim.startswith(item.label:lower(), prefix:lower())
28-
end, items),
29-
})
30-
end)
31-
end
32-
33-
---@param completion_item lsp.CompletionItem
34-
---@param callback fun(completion_item: lsp.CompletionItem|nil)
35-
function Source:execute(completion_item, callback)
36-
callback(completion_item)
37-
vim.api.nvim_set_option_value('buflisted', false, { buf = 0 })
38-
end
1+
local utils = require('CopilotChat.utils')
392

403
local M = {}
414

42-
--- Setup the nvim-cmp source for copilot-chat window
435
function M.setup()
44-
cmp.register_source('copilot-chat', Source)
45-
cmp.setup.filetype('copilot-chat', {
46-
sources = {
47-
{ name = 'copilot-chat' },
48-
},
49-
})
6+
utils.deprecate('CopilotChat.integrations.cmp.setup', 'config.chat_autocomplete=true')
507
end
518

529
return M

lua/CopilotChat/utils.lua

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
local log = require('plenary.log')
21
local M = {}
32

43
--- Create class
@@ -74,6 +73,23 @@ function M.table_equals(a, b)
7473
return true
7574
end
7675

76+
--- Blend a color with the neovim background
77+
function M.blend_color_with_neovim_bg(color_name, blend)
78+
local color_int = vim.api.nvim_get_hl(0, { name = color_name }).fg
79+
local bg_int = vim.api.nvim_get_hl(0, { name = 'Normal' }).bg
80+
81+
if not color_int or not bg_int then
82+
return
83+
end
84+
85+
local color = { (color_int / 65536) % 256, (color_int / 256) % 256, color_int % 256 }
86+
local bg = { (bg_int / 65536) % 256, (bg_int / 256) % 256, bg_int % 256 }
87+
local r = math.floor((color[1] * blend + bg[1] * (100 - blend)) / 100)
88+
local g = math.floor((color[2] * blend + bg[2] * (100 - blend)) / 100)
89+
local b = math.floor((color[3] * blend + bg[3] * (100 - blend)) / 100)
90+
return string.format('#%02x%02x%02x', r, g, b)
91+
end
92+
7793
--- Return to normal mode
7894
function M.return_to_normal_mode()
7995
local mode = vim.fn.mode():lower()
@@ -84,8 +100,17 @@ function M.return_to_normal_mode()
84100
end
85101
end
86102

103+
--- Mark a function as deprecated
87104
function M.deprecate(old, new)
88-
vim.deprecate(old, new, '3.0.0', 'CopilotChat.nvim', false)
105+
vim.deprecate(old, new, '3.0.X', 'CopilotChat.nvim', false)
106+
end
107+
108+
--- Debounce a function
109+
function M.debounce(fn, delay)
110+
if M.timer then
111+
M.timer:stop()
112+
end
113+
M.timer = vim.defer_fn(fn, delay)
89114
end
90115

91116
return M

0 commit comments

Comments
 (0)