From 4db1e4744cd666b25aaeb6f22a25ec60084c383e Mon Sep 17 00:00:00 2001 From: gptlang <121417512+gptlang@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:27:05 +0800 Subject: [PATCH 001/338] Allow runtime selection of models with `:CopilotChatModels` (#368) * feat: Model selector (initial commit) * fix/model-prompt: with auth * fix: schedule stuff for async stuff? * fix: increase scope if local scope not found? * fix: only show chat models * model selector: cache model list * move vim logic out of copilot.lua & reduce code repetition * fix: cache is nil when it shouldn't be due to callback * remove duplicate entries --- lua/CopilotChat/copilot.lua | 54 ++++++++++++++++++++++++++++++++++++ lua/CopilotChat/init.lua | 16 ++++++++++- lua/CopilotChat/tiktoken.lua | 8 ++++-- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index f20103fe..2a954fee 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -32,6 +32,7 @@ ---@field save fun(self: CopilotChat.Copilot, name: string, path: string):nil ---@field load fun(self: CopilotChat.Copilot, name: string, path: string):table ---@field running fun(self: CopilotChat.Copilot):boolean +---@field select_model fun(self: CopilotChat.Copilot, callback: fun(table):nil):nil local log = require('plenary.log') local curl = require('plenary.curl') @@ -279,6 +280,7 @@ local Copilot = class(function(self, proxy, allow_insecure) self.sessionid = nil self.machineid = machine_id() self.current_job = nil + self.models_cache = nil end) function Copilot:with_auth(on_done, on_error) @@ -499,6 +501,58 @@ function Copilot:ask(prompt, opts) end, on_error) end +--- Fetch & allow model selection +---@param callback fun(table):nil +function Copilot:select_model(callback) + if self.models_cache ~= nil then + vim.schedule(function() + callback(self.models_cache) + end) + return + end + local url = 'https://api.githubcopilot.com/models' + self:with_auth(function() + local headers = generate_headers(self.token.token, self.sessionid, self.machineid) + curl.get(url, { + headers = headers, + proxy = self.proxy, + insecure = self.allow_insecure, + on_error = function(err) + err = 'Failed to get response: ' .. vim.inspect(err) + log.error(err) + end, + callback = function(response) + if response.status ~= 200 then + local msg = 'Failed to fetch models: ' .. tostring(response.status) + log.error(msg) + return + end + + local models = vim.json.decode(response.body)['data'] + local selections = {} + for _, model in ipairs(models) do + if model['capabilities']['type'] == 'chat' then + table.insert(selections, model['version']) + end + end + -- Remove duplicates from selection + local hash = {} + selections = vim.tbl_filter(function(model) + if not hash[model] then + hash[model] = true + return true + end + return false + end, selections) + self.models_cache = selections + vim.schedule(function() + callback(self.models_cache) + end) + end, + }) + end) +end + --- Generate embeddings for the given inputs ---@param inputs table: The inputs to embed ---@param opts CopilotChat.copilot.embed.opts: Options for the request diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index b1d95fa6..0370f4bf 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -354,11 +354,21 @@ function M.toggle(config, source) end end --- @returns string +--- @returns string function M.response() return state.response end +function M.select_model() + state.copilot:select_model(function(models) + vim.ui.select(models, { + prompt = 'Select a model', + }, function(choice) + M.config.model = choice + end) + end) +end + --- Ask a question to the Copilot model. ---@param prompt string ---@param config CopilotChat.config|CopilotChat.config.prompt|nil @@ -859,6 +869,10 @@ function M.setup(config) range = true, }) + vim.api.nvim_create_user_command('CopilotChatModels', function() + M.select_model() + end, { force = true }) + vim.api.nvim_create_user_command('CopilotChatOpen', function() M.open() end, { force = true }) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 89f6269b..f751cc00 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -30,9 +30,11 @@ local function load_tiktoken_data(done, model) -- Take filename after the last slash of the url local cache_path = get_cache_path(tiktoken_url:match('.+/(.+)')) if not file_exists(cache_path) then - curl.get(tiktoken_url, { - output = cache_path, - }) + vim.schedule(function() + curl.get(tiktoken_url, { + output = cache_path, + }) + end) end done(cache_path) From 1806ecfc76ea5ca83e7211d78c41c110cdd08591 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Jul 2024 15:27:27 +0000 Subject: [PATCH 002/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 26496c6b..079aa9ab 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 14 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 17 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 0de7dc5bd54c0382b13dd69621ec2b4ca42da8b2 Mon Sep 17 00:00:00 2001 From: gptlang <121417512+gptlang@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:29:03 +0800 Subject: [PATCH 003/338] Readme: document `:CopilotChatModels` --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c3b55117..04ad4941 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/ma - `:CopilotChatSave ?` - Save chat history to file - `:CopilotChatLoad ?` - Load chat history from file - `:CopilotChatDebugInfo` - Show debug information +- `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. #### Commands coming from default prompts From 27c7be529ad2526c4b3a40879301603bd5878001 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Jul 2024 15:29:29 +0000 Subject: [PATCH 004/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 079aa9ab..26d089a5 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -127,6 +127,7 @@ COMMANDS ~ - `:CopilotChatSave ?` - Save chat history to file - `:CopilotChatLoad ?` - Load chat history from file - `:CopilotChatDebugInfo` - Show debug information +- `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. COMMANDS COMING FROM DEFAULT PROMPTS From a6d74d1de3b72794bf3445f07e3658a9b0d8eabe Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 17 Jul 2024 19:06:53 +0200 Subject: [PATCH 005/338] fix: Handle layout replace closing, handle empty embed results Also update documentation for default accept binding and general small cleanup. Closes #359 Closes #350 Closes #357 Closes #309 Signed-off-by: Tomas Slusny --- README.md | 2 +- lua/CopilotChat/chat.lua | 12 +++++++++--- lua/CopilotChat/context.lua | 4 ++++ lua/CopilotChat/copilot.lua | 14 ++++++++------ lua/CopilotChat/init.lua | 15 +++++++++------ lua/CopilotChat/overlay.lua | 2 +- 6 files changed, 32 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 04ad4941..077b326b 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ Also see [here](/lua/CopilotChat/config.lua): }, submit_prompt = { normal = '', - insert = '' + insert = '' }, accept_diff = { normal = '', diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 469fcb04..ccdbcaf8 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -10,7 +10,7 @@ ---@field last fun(self: CopilotChat.Chat) ---@field clear fun(self: CopilotChat.Chat) ---@field open fun(self: CopilotChat.Chat, config: CopilotChat.config) ----@field close fun(self: CopilotChat.Chat) +---@field close fun(self: CopilotChat.Chat, bufnr: number?) ---@field focus fun(self: CopilotChat.Chat) ---@field follow fun(self: CopilotChat.Chat) ---@field finish fun(self: CopilotChat.Chat, msg: string?) @@ -40,6 +40,7 @@ local Chat = class(function(self, help, on_buf_create) self.winnr = nil self.spinner = nil self.separator = nil + self.layout = nil self.buf_create = function() local bufnr = vim.api.nvim_create_buf(false, true) @@ -192,6 +193,7 @@ function Chat:open(config) vim.api.nvim_win_set_buf(self.winnr, self.bufnr) end + self.layout = layout self.separator = config.separator vim.wo[self.winnr].wrap = true @@ -209,13 +211,17 @@ function Chat:open(config) self:render() end -function Chat:close() +function Chat:close(bufnr) if self.spinner then self.spinner:finish() end if self:visible() then - vim.api.nvim_win_close(self.winnr, true) + if self.layout == 'replace' then + self:restore(self.winnr, bufnr) + else + vim.api.nvim_win_close(self.winnr, true) + end self.winnr = nil end end diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 65dff289..8b8a4016 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -248,6 +248,10 @@ function M.find_for_query(copilot, opts) on_error = on_error, on_done = function(query_out) local query = query_out[1] + if not query then + on_done({}) + return + end log.debug('Prompt:', query.prompt) log.debug('Content:', query.content) local data = data_ranked_by_relatedness(query, out, 20) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 2a954fee..01031f98 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -43,6 +43,7 @@ local temp_file = utils.temp_file local prompts = require('CopilotChat.prompts') local tiktoken = require('CopilotChat.tiktoken') local max_tokens = 8192 +local timeout = 30000 local version_headers = { ['editor-version'] = 'Neovim/' .. vim.version().major @@ -309,6 +310,7 @@ function Copilot:with_auth(on_done, on_error) end curl.get(url, { + timeout = timeout, headers = headers, proxy = self.proxy, insecure = self.allow_insecure, @@ -421,6 +423,7 @@ function Copilot:ask(prompt, opts) local headers = generate_headers(self.token.token, self.sessionid, self.machineid) self.current_job = curl .post(url, { + timeout = timeout, headers = headers, body = temp_file(body), proxy = self.proxy, @@ -505,15 +508,15 @@ end ---@param callback fun(table):nil function Copilot:select_model(callback) if self.models_cache ~= nil then - vim.schedule(function() - callback(self.models_cache) - end) + callback(self.models_cache) return end + local url = 'https://api.githubcopilot.com/models' self:with_auth(function() local headers = generate_headers(self.token.token, self.sessionid, self.machineid) curl.get(url, { + timeout = timeout, headers = headers, proxy = self.proxy, insecure = self.allow_insecure, @@ -545,9 +548,7 @@ function Copilot:select_model(callback) return false end, selections) self.models_cache = selections - vim.schedule(function() - callback(self.models_cache) - end) + callback(self.models_cache) end, }) end) @@ -584,6 +585,7 @@ function Copilot:embed(inputs, opts) table.insert(jobs, function(resolve) local headers = generate_headers(self.token.token, self.sessionid, self.machineid) curl.post(url, { + timeout = timeout, headers = headers, body = temp_file(body), proxy = self.proxy, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0370f4bf..b7df7523 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -340,7 +340,7 @@ end --- Close the chat window. function M.close() vim.cmd('stopinsert') - state.chat:close() + state.chat:close(state.source and state.source.bufnr or nil) end --- Toggle the chat window. @@ -359,12 +359,15 @@ function M.response() return state.response end +--- Select a Copilot GPT model. function M.select_model() state.copilot:select_model(function(models) - vim.ui.select(models, { - prompt = 'Select a model', - }, function(choice) - M.config.model = choice + vim.schedule(function() + vim.ui.select(models, { + prompt = 'Select a model', + }, function(choice) + M.config.model = choice + end) end) end) end @@ -712,7 +715,7 @@ function M.setup(config) end if state.chat then - state.chat:close() + state.chat:close(state.source and state.source.bufnr or nil) state.chat:delete() end state.chat = Chat(chat_help, function(bufnr) diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index 804e0709..1c12b442 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -63,7 +63,7 @@ end function Overlay:restore(winnr, bufnr) self.current = nil - vim.api.nvim_win_set_buf(winnr, bufnr) + vim.api.nvim_win_set_buf(winnr, bufnr or 0) vim.api.nvim_win_set_hl_ns(winnr, 0) end From 92bc7b5e564c23b12b2ed41dd7657fdafe39d95f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Jul 2024 17:13:42 +0000 Subject: [PATCH 006/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 26d089a5..a68ba4a2 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -318,7 +318,7 @@ Also see here : }, submit_prompt = { normal = '', - insert = '' + insert = '' }, accept_diff = { normal = '', From e195e53a2fd60c6b18037a9151ee78fd81cc0bc9 Mon Sep 17 00:00:00 2001 From: gptlang Date: Tue, 23 Jul 2024 00:19:26 +0800 Subject: [PATCH 007/338] fix #261: initiallize tiktoken from scheduled thread if not downloaded (during first use only) --- lua/CopilotChat/tiktoken.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index f751cc00..f7639548 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -34,10 +34,11 @@ local function load_tiktoken_data(done, model) curl.get(tiktoken_url, { output = cache_path, }) + done(cache_path) end) + else + done(cache_path) end - - done(cache_path) async:close() end) async:send() From 5a019498c98352c6cc560257931ee8f56af840dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Jul 2024 16:19:55 +0000 Subject: [PATCH 008/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index a68ba4a2..7c6fff40 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 17 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 22 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From f7861cb7d0ea46d57f67595876c8d2835eae29a2 Mon Sep 17 00:00:00 2001 From: gptlang Date: Tue, 23 Jul 2024 00:30:01 +0800 Subject: [PATCH 009/338] fix #373: pass in blank prompt if nil to split --- lua/CopilotChat/integrations/telescope.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/integrations/telescope.lua b/lua/CopilotChat/integrations/telescope.lua index 18c905bb..0d733053 100644 --- a/lua/CopilotChat/integrations/telescope.lua +++ b/lua/CopilotChat/integrations/telescope.lua @@ -34,7 +34,7 @@ function M.pick(pick_actions, opts) 0, -1, false, - vim.split(pick_actions.actions[entry[1]].prompt, '\n') + vim.split(pick_actions.actions[entry[1]].prompt or '', '\n') ) end, }), From bcddeb70557cf8fb4835c8c9fd5bdb155cd2b5e1 Mon Sep 17 00:00:00 2001 From: jinzhongjia Date: Fri, 26 Jul 2024 10:10:38 +0800 Subject: [PATCH 010/338] Add `tiktoken_core` installation method (#374) For Arch Linux user, we can install it from aur! --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 077b326b..33d95356 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ Optional: - tiktoken_core: `sudo luarocks install --lua-version 5.1 tiktoken_core`. Alternatively, download a pre-built binary from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases) - You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. +> For Arch Linux user, you can install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from aur! + ## Installation ### Lazy.nvim From 79dfc060d4a35fdbcf319972a099fcfc6ec7d511 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Jul 2024 02:11:00 +0000 Subject: [PATCH 011/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 7c6fff40..c98ac5b9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 22 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 26 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -39,6 +39,11 @@ Optional: - You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. + For Arch Linux user, you can install `luajit-tiktoken-bin` + or + `lua51-tiktoken-bin` + from aur! + INSTALLATION *CopilotChat-installation* From f20a0425b33c1704133bdef5ec10c4e94f6efdc6 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:11:23 +0800 Subject: [PATCH 012/338] docs: add jinzhongjia as a contributor for doc (#376) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 78f2dd4a..3242d568 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -207,6 +207,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/6877858?v=4", "profile": "http://dimalip.in", "contributions": ["code"] + }, + { + "login": "jinzhongjia", + "name": "jinzhongjia", + "avatar_url": "https://avatars.githubusercontent.com/u/41784264?v=4", + "profile": "https://nvimer.org", + "contributions": ["doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 33d95356..3f4e6841 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-29-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square)](#contributors-) @@ -594,6 +594,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Dmitrii Lipin
Dmitrii Lipin

💻 + jinzhongjia
jinzhongjia

📖 From 2c4d2954f9b56945dc8d053903b16759d08f9f37 Mon Sep 17 00:00:00 2001 From: gptlang <121417512+gptlang@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:42:08 +0000 Subject: [PATCH 013/338] Use gpt-4o by default (#377) * use gpt-4o by default! * make tiktoken less dependent on model version/dates --- README.md | 2 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/copilot.lua | 2 +- lua/CopilotChat/init.lua | 3 +++ lua/CopilotChat/tiktoken.lua | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3f4e6841..9abbcedd 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Also see [here](/lua/CopilotChat/config.lua): allow_insecure = false, -- Allow insecure server connections system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4', -- GPT model to use, 'gpt-3.5-turbo' or 'gpt-4' + model = 'gpt-4o', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or 'gpt-4o' temperature = 0.1, -- GPT temperature question_header = '## User ', -- Header to use for user questions diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index c962264e..5fa09cd7 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -83,7 +83,7 @@ return { allow_insecure = false, -- Allow insecure server connections system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4', -- GPT model to use, 'gpt-3.5-turbo' or 'gpt-4' + model = 'gpt-4o-2024-05-13', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or `gpt-4o-2024-05-13` temperature = 0.1, -- GPT temperature question_header = '## User ', -- Header to use for user questions diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 01031f98..eebf1314 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -353,7 +353,7 @@ function Copilot:ask(prompt, opts) local start_row = opts.start_row or 0 local end_row = opts.end_row or 0 local system_prompt = opts.system_prompt or prompts.COPILOT_INSTRUCTIONS - local model = opts.model or 'gpt-4' + local model = opts.model or 'gpt-4o-2024-05-13' local temperature = opts.temperature or 0.1 local on_done = opts.on_done local on_progress = opts.on_progress diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index b7df7523..1e39bc0f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -606,6 +606,9 @@ function M.setup(config) end M.config = vim.tbl_deep_extend('force', default_config, config or {}) + if M.config.model == 'gpt-4o' then + M.config.model = 'gpt-4o-2024-05-13' + end if state.copilot then state.copilot:stop() diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index f7639548..7e59e069 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -22,7 +22,7 @@ end local function load_tiktoken_data(done, model) local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken' -- If model is gpt-4o, use o200k_base.tiktoken - if model == 'gpt-4o' then + if vim.startswith(model, 'gpt-4o') then tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/o200k_base.tiktoken' end local async From 8a8a60fea57f5caf9f67d4385c5326669b516027 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 28 Jul 2024 15:42:30 +0000 Subject: [PATCH 014/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index c98ac5b9..49950daa 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 26 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 28 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -229,7 +229,7 @@ Also see here : allow_insecure = false, -- Allow insecure server connections system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4', -- GPT model to use, 'gpt-3.5-turbo' or 'gpt-4' + model = 'gpt-4o', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or 'gpt-4o' temperature = 0.1, -- GPT temperature question_header = '## User ', -- Header to use for user questions @@ -580,7 +580,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -595,7 +595,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-29-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From 45318b22674732498e61f7754987aa877dd92532 Mon Sep 17 00:00:00 2001 From: Dung Huynh Duc Date: Mon, 29 Jul 2024 21:16:31 +0800 Subject: [PATCH 015/338] fix(tiktoken): add nil check for model before call tiktoken --- lua/CopilotChat/tiktoken.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 7e59e069..e19a39da 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -22,7 +22,7 @@ end local function load_tiktoken_data(done, model) local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken' -- If model is gpt-4o, use o200k_base.tiktoken - if vim.startswith(model, 'gpt-4o') then + if model ~= nil and vim.startswith(model, 'gpt-4o') then tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/o200k_base.tiktoken' end local async From 4a5e07185b37d3132e5541d8fa42aa874b774476 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Jul 2024 13:17:00 +0000 Subject: [PATCH 016/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 49950daa..8fcb3127 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 28 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 29 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 34dc3cfa36bfe22021f3fb03d36d6cbf00c179de Mon Sep 17 00:00:00 2001 From: guill Date: Mon, 5 Aug 2024 20:14:46 -0700 Subject: [PATCH 017/338] Improve completion of Copilot keywords (#384) * Improve completion of Copilot keywords Instead of only triggering completion with '/' or '@', completion can also be manually triggered. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- lua/CopilotChat/integrations/cmp.lua | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lua/CopilotChat/integrations/cmp.lua b/lua/CopilotChat/integrations/cmp.lua index feb9eaaf..b3027673 100644 --- a/lua/CopilotChat/integrations/cmp.lua +++ b/lua/CopilotChat/integrations/cmp.lua @@ -7,25 +7,29 @@ function Source:get_trigger_characters() return { '@', '/' } end +function Source:get_keyword_pattern() + return [[\%(@\|/\)\k*]] +end + function Source:complete(params, callback) local items = {} local prompts_to_use = chat.prompts() - if params.completion_context.triggerCharacter == '/' then - for name, _ in pairs(prompts_to_use) do + local prefix = string.lower(params.context.cursor_before_line:sub(params.offset)) + local prefix_len = #prefix + local checkAdd = function(word) + if word:lower():sub(1, prefix_len) == prefix then items[#items + 1] = { - label = '/' .. name, + label = word, + kind = cmp.lsp.CompletionItemKind.Keyword, } end - else - items[#items + 1] = { - label = '@buffers', - } - - items[#items + 1] = { - label = '@buffer', - } end + for name, _ in pairs(prompts_to_use) do + checkAdd('/' .. name) + end + checkAdd('@buffers') + checkAdd('@buffer') callback({ items = items }) end From 8a9c56d6abbe3197464e7c0b022d4d7aeb74eab7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Aug 2024 03:15:05 +0000 Subject: [PATCH 018/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 8fcb3127..16b6095b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 July 29 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 06 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From cfdf371cec954fccf5410315884e110d214d38fa Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:15:23 +0800 Subject: [PATCH 019/338] docs: add guill as a contributor for code (#385) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3242d568..8019c9ce 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -214,6 +214,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/41784264?v=4", "profile": "https://nvimer.org", "contributions": ["doc"] + }, + { + "login": "guill", + "name": "guill", + "avatar_url": "https://avatars.githubusercontent.com/u/3157454?v=4", + "profile": "https://github.com/guill", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9abbcedd..6d6d8829 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square)](#contributors-) @@ -595,6 +595,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Dmitrii Lipin
Dmitrii Lipin

💻 jinzhongjia
jinzhongjia

📖 + guill
guill

💻 From 0149238e8aea9e1a65bfdb08e4f0a9688ecb5664 Mon Sep 17 00:00:00 2001 From: Sjon-Paul Brown <81941908+sjonpaulbrown-cc@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:42:38 -0600 Subject: [PATCH 020/338] Add support for overriding the default yank_diff register (#389) --- README.md | 2 ++ lua/CopilotChat/config.lua | 3 +++ lua/CopilotChat/init.lua | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d6d8829..4e0e77b6 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,8 @@ Also see [here](/lua/CopilotChat/config.lua): proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections + yank_diff_register = '"', -- Allow overriding the register for yanking diffs + system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or 'gpt-4o' temperature = 0.1, -- GPT temperature diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 5fa09cd7..644c946d 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -58,6 +58,7 @@ local select = require('CopilotChat.select') ---@field proxy string? ---@field allow_insecure boolean? ---@field system_prompt string? +---@field yank_diff_register string? ---@field model string? ---@field temperature number? ---@field question_header string? @@ -82,6 +83,8 @@ return { proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections + yank_diff_register = '"', -- Allows overriding the register for yanking diffs + system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o-2024-05-13', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or `gpt-4o-2024-05-13` temperature = 0.1, -- GPT temperature diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 1e39bc0f..1c2cf88c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -774,7 +774,7 @@ function M.setup(config) local lines = find_lines_between_separator(section_lines, '^```%w*$', true) if #lines > 0 then local content = table.concat(lines, '\n') - vim.fn.setreg('"', content) + vim.fn.setreg(M.config.yank_diff_register, content) end end) From dbbe5a0c938e26fc8b6353fcccdfd8fba3955048 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 Aug 2024 14:43:00 +0000 Subject: [PATCH 021/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 16b6095b..0d15ed67 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 06 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 09 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -228,6 +228,8 @@ Also see here : proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections + yank_diff_register = '"', -- Allow overriding the register for yanking diffs + system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or 'gpt-4o' temperature = 0.1, -- GPT temperature @@ -580,7 +582,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -595,7 +597,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From bddda2aaf66d34bf6dbae8af26166187a4343cca Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:43:50 +0800 Subject: [PATCH 022/338] docs: add sjonpaulbrown-cc as a contributor for code (#390) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8019c9ce..68197c9f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -221,6 +221,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/3157454?v=4", "profile": "https://github.com/guill", "contributions": ["code"] + }, + { + "login": "sjonpaulbrown-cc", + "name": "Sjon-Paul Brown", + "avatar_url": "https://avatars.githubusercontent.com/u/81941908?v=4", + "profile": "https://github.com/sjonpaulbrown-cc", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4e0e77b6..e6f4dfb9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-32-orange.svg?style=flat-square)](#contributors-) @@ -598,6 +598,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Dmitrii Lipin
Dmitrii Lipin

💻 jinzhongjia
jinzhongjia

📖 guill
guill

💻 + Sjon-Paul Brown
Sjon-Paul Brown

💻 From 320b853c25c781dc572e4c19d0df25ea38b2e480 Mon Sep 17 00:00:00 2001 From: gptlang <121417512+gptlang@users.noreply.github.com> Date: Sat, 17 Aug 2024 13:59:07 +0000 Subject: [PATCH 023/338] Provide auth instructions for both cp.vim/lua. close #392 --- lua/CopilotChat/copilot.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index eebf1314..61301142 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -287,7 +287,7 @@ end) function Copilot:with_auth(on_done, on_error) if not self.github_token then local msg = - 'No GitHub token found, please use `:Copilot setup` to set it up from copilot.vim or copilot.lua' + 'No GitHub token found, please use `:Copilot auth` to set it up from copilot.lua or `:Copilot setup` for copilot.vim' log.error(msg) if on_error then on_error(msg) From 1e2c9a14485472bdc7285c1846a9157a9953eab7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 17 Aug 2024 13:59:29 +0000 Subject: [PATCH 024/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 0d15ed67..46461b0d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 09 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 17 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -582,7 +582,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -597,7 +597,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-32-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From d1c2b74963d815c02d32a4ae8de7e527888ed8e0 Mon Sep 17 00:00:00 2001 From: gptlang <121417512+gptlang@users.noreply.github.com> Date: Sat, 17 Aug 2024 14:13:49 +0000 Subject: [PATCH 025/338] docs: how to use custom prompts closes #381 --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e6f4dfb9..0868aa02 100644 --- a/README.md +++ b/README.md @@ -365,14 +365,19 @@ You can define custom system prompts by using `system_prompt` property when pass { system_prompt = 'Your name is Github Copilot and you are a AI assistant for developers.', prompts = { - MyCustomPromptWithCustomSystemPrompt = { + Johnny = { system_prompt = 'Your name is Johny Microsoft and you are not an AI assistant for developers.', prompt = 'Explain how it works.', }, + Yarrr = { + system_prompt = '[[You are fascinated by pirates, so please respond in pirate speak.]]' + }, }, } ``` +To use any of your custom prompts, simply do `:CopilotChat`. E.g. `:CopilotChatJohnny` or `:CopilotChatYarrr What is a sorting algo?`. Tab autocomplete will help you out. + ### Customizing buffers You can set local options for the buffers that are created by this plugin: `copilot-diff`, `copilot-system-prompt`, `copilot-user-selection`, `copilot-chat`. From d459297535c23aa400f400543f74535a6337b4b8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 17 Aug 2024 14:14:08 +0000 Subject: [PATCH 026/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 46461b0d..49892e69 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -402,14 +402,21 @@ passing config around. { system_prompt = 'Your name is Github Copilot and you are a AI assistant for developers.', prompts = { - MyCustomPromptWithCustomSystemPrompt = { + Johnny = { system_prompt = 'Your name is Johny Microsoft and you are not an AI assistant for developers.', prompt = 'Explain how it works.', }, + Yarrr = { + system_prompt = '[[You are fascinated by pirates, so please respond in pirate speak.]]' + }, }, } < +To use any of your custom prompts, simply do `:CopilotChat`. E.g. +`:CopilotChatJohnny` or `:CopilotChatYarrr What is a sorting algo?`. Tab +autocomplete will help you out. + CUSTOMIZING BUFFERS ~ From 4b9ca95bbc737a94e8be582716a04e827911b9b6 Mon Sep 17 00:00:00 2001 From: gptlang Date: Sat, 17 Aug 2024 22:54:01 +0800 Subject: [PATCH 027/338] fix #381: Adhere to system prompts in /prompts --- lua/CopilotChat/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 1c2cf88c..9bb7c80b 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -102,6 +102,7 @@ local function update_prompts(prompt, system_prompt) if out and string.match(out, [[/[%w_]+]]) then try_again = true end + system_prompt = found.system_prompt or system_prompt return out elseif found.kind == 'system' then system_prompt = found.prompt From 04687697ec73383d7c25ea48f9b53fac5ff4d80b Mon Sep 17 00:00:00 2001 From: GitMurf <64155612+GitMurf@users.noreply.github.com> Date: Sun, 18 Aug 2024 06:05:26 -0700 Subject: [PATCH 028/338] fix(readme): unnecessary [[brackets]] around pirate system prompt example (#394) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0868aa02..44d70038 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,7 @@ You can define custom system prompts by using `system_prompt` property when pass prompt = 'Explain how it works.', }, Yarrr = { - system_prompt = '[[You are fascinated by pirates, so please respond in pirate speak.]]' + system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.' }, }, } From ca595e16714a47d674975a082601c54b4f9a3cf1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Aug 2024 13:05:45 +0000 Subject: [PATCH 029/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 49892e69..21869bd9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 17 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 18 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -407,7 +407,7 @@ passing config around. prompt = 'Explain how it works.', }, Yarrr = { - system_prompt = '[[You are fascinated by pirates, so please respond in pirate speak.]]' + system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.' }, }, } From 1a92bb6d69b35bbfe32a57f81dc4f254abb56352 Mon Sep 17 00:00:00 2001 From: gptlang <121417512+gptlang@users.noreply.github.com> Date: Sun, 18 Aug 2024 13:12:37 +0000 Subject: [PATCH 030/338] CI: change lua to luajit-openresty to fix download fails --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb1da9c0..cded4653 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: - name: luajit uses: leafo/gh-actions-lua@v10 with: - luaVersion: "luajit-2.1.0-beta3" + luaVersion: "luajit-openresty" - name: luarocks uses: leafo/gh-actions-luarocks@v4 From b82e1fd9d84e84971fe56564e604cc432b5e06f8 Mon Sep 17 00:00:00 2001 From: gptlang Date: Thu, 22 Aug 2024 18:25:52 +0800 Subject: [PATCH 031/338] show which model is currently being used --- lua/CopilotChat/init.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9bb7c80b..7198c61c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -876,6 +876,11 @@ function M.setup(config) range = true, }) + vim.api.nvim_create_user_command('CopilotChatModel', function() + -- Show which model is being used in an alert + vim.notify('Using model: ' .. M.config.model, vim.log.levels.INFO) + end, { force = true }) + vim.api.nvim_create_user_command('CopilotChatModels', function() M.select_model() end, { force = true }) From 36cb5c89f3ac551b8130b3b9651b83b216da6c5e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 22 Aug 2024 10:27:02 +0000 Subject: [PATCH 032/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 21869bd9..295cf738 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 18 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 22 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 25d75227b164aabf6258a2d8825319949b798c23 Mon Sep 17 00:00:00 2001 From: gptlang Date: Thu, 22 Aug 2024 18:27:47 +0800 Subject: [PATCH 033/338] `:CopilotChatModel` - show which model is currently being used --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 44d70038..c1a30bb7 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/ma - `:CopilotChatLoad ?` - Load chat history from file - `:CopilotChatDebugInfo` - Show debug information - `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. +- `:CopilotChatModel` - View the currently selected model. #### Commands coming from default prompts From c78dd206a7106eef97e990e00767bef0370f78e2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 22 Aug 2024 10:28:33 +0000 Subject: [PATCH 034/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 295cf738..ec88643b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -133,6 +133,7 @@ COMMANDS ~ - `:CopilotChatLoad ?` - Load chat history from file - `:CopilotChatDebugInfo` - Show debug information - `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. +- `:CopilotChatModel` - View the currently selected model. COMMANDS COMING FROM DEFAULT PROMPTS From 83bce52db3526fcb222921feccd790c3055d51c8 Mon Sep 17 00:00:00 2001 From: gptlang Date: Thu, 22 Aug 2024 18:56:16 +0800 Subject: [PATCH 035/338] install: install tiktoken with makefile --- Makefile | 59 +++++++++++++++++++++++++++++++++++++++++++++---------- README.md | 1 + 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 86f44cfd..8be81f6a 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,64 @@ -.PHONY: help +UNAME := $(shell uname) +ARCH := $(shell uname -m) + +ifeq ($(UNAME), Linux) + OS := linux + EXT := so +else ifeq ($(UNAME), Darwin) + OS := macOS + EXT := dylib +else + $(error Unsupported operating system: $(UNAME)) +endif + +LUA_VERSIONS := luajit lua51 +BUILD_DIR := build + +.PHONY: help install-cli install-pre-commit install test tiktoken clean + help: - @echo "Available commands:" - @echo " install-cli - Install Lua and Luarocks using Homebrew" - @echo " install-pre-commit - Install pre-commit using pip" - @echo " install - Install vusted using Luarocks" - @echo " test - Run tests using vusted" + @echo "Available commands:" + @echo " install-cli - Install Lua and Luarocks using Homebrew" + @echo " install-pre-commit - Install pre-commit using pip" + @echo " install - Install vusted using Luarocks" + @echo " test - Run tests using vusted" + @echo " tiktoken - Download tiktoken_core library" + @echo " clean - Remove build directory" -.PHONY: install-cli install-cli: brew install luarocks brew install lua -.PHONY: install-pre-commit install-pre-commit: pip install pre-commit pre-commit install -.PHONY: install install: luarocks install vusted -.PHONY: test test: vusted test + +all: luajit + +luajit: $(BUILD_DIR)/tiktoken_core.$(EXT) +lua51: $(BUILD_DIR)/tiktoken_core-lua51.$(EXT) + + +define download_release + curl -L https://github.com/gptlang/lua-tiktoken/releases/latest/download/tiktoken_core-$(1)-$(2).$(EXT) -o $(3) +endef + +$(BUILD_DIR)/tiktoken_core.$(EXT): | $(BUILD_DIR) + $(call download_release,$(OS),luajit,$@) + +$(BUILD_DIR)/tiktoken_core-lua51.$(EXT): | $(BUILD_DIR) + $(call download_release,$(OS),lua51,$@) + +tiktoken: $(BUILD_DIR)/tiktoken_core.$(EXT) $(BUILD_DIR)/tiktoken_core-lua51.$(EXT) + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +clean: + rm -rf $(BUILD_DIR) diff --git a/README.md b/README.md index c1a30bb7..f340f46d 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ return { { "zbirenbaum/copilot.lua" }, -- or github/copilot.vim { "nvim-lua/plenary.nvim" }, -- for curl, log wrapper }, + build = "make tiktoken", -- Only on MacOS or Linux opts = { debug = true, -- Enable debugging -- See Configuration section for rest From 43d033b68c8bede4cc87092c7db6bb3bbb2fe145 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 22 Aug 2024 10:56:46 +0000 Subject: [PATCH 036/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ec88643b..73d61ff3 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -58,6 +58,7 @@ LAZY.NVIM ~ { "zbirenbaum/copilot.lua" }, -- or github/copilot.vim { "nvim-lua/plenary.nvim" }, -- for curl, log wrapper }, + build = "make tiktoken", -- Only on MacOS or Linux opts = { debug = true, -- Enable debugging -- See Configuration section for rest From 1c67656d11d45680965fbc92316c81419bb693b2 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 29 Aug 2024 04:40:42 +0200 Subject: [PATCH 037/338] Cleanup auto insert behaviour and add support for insert at end - Cleanup auto insert behaviour and move it to chat.lua - Add support for moving cursor to end of buffer before entering insert mode in the chat buffer - Cleanup configuration (move yank register config near the mapping) Closes #379 Signed-off-by: Tomas Slusny --- README.md | 6 +- lua/CopilotChat/chat.lua | 18 ++++- lua/CopilotChat/config.lua | 9 ++- lua/CopilotChat/init.lua | 80 +++++++++++----------- lua/CopilotChat/integrations/fzflua.lua | 2 +- lua/CopilotChat/integrations/telescope.lua | 2 +- lua/CopilotChat/utils.lua | 9 ++- 7 files changed, 71 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index f340f46d..960fdb2b 100644 --- a/README.md +++ b/README.md @@ -201,8 +201,6 @@ Also see [here](/lua/CopilotChat/config.lua): proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections - yank_diff_register = '"', -- Allow overriding the register for yanking diffs - system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or 'gpt-4o' temperature = 0.1, -- GPT temperature @@ -215,7 +213,8 @@ Also see [here](/lua/CopilotChat/config.lua): show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and if auto follow cursor is enabled on new prompt + auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + insert_at_end = false, -- Move cursor to end of buffer when inserting text clear_chat_on_new_prompt = false, -- Clears chat on every new prompt highlight_selection = true, -- Highlight selection in the source buffer when in the chat window @@ -306,6 +305,7 @@ Also see [here](/lua/CopilotChat/config.lua): }, yank_diff = { normal = 'gy', + register = '"', }, show_diff = { normal = 'gd' diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index ccdbcaf8..9a494c0f 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -32,9 +32,10 @@ function CopilotChatFoldExpr(lnum, separator) return '=' end -local Chat = class(function(self, help, on_buf_create) +local Chat = class(function(self, help, auto_insert, on_buf_create) self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') self.help = help + self.auto_insert = auto_insert self.on_buf_create = on_buf_create self.bufnr = nil self.winnr = nil @@ -118,6 +119,10 @@ end function Chat:append(str) self:validate() + if self:active() then + utils.return_to_normal_mode() + end + if self.spinner then self.spinner:start() end @@ -217,11 +222,16 @@ function Chat:close(bufnr) end if self:visible() then + if self:active() then + utils.return_to_normal_mode() + end + if self.layout == 'replace' then self:restore(self.winnr, bufnr) else vim.api.nvim_win_close(self.winnr, true) end + self.winnr = nil end end @@ -229,6 +239,9 @@ end function Chat:focus() if self:visible() then vim.api.nvim_set_current_win(self.winnr) + if self.auto_insert and self:active() then + vim.cmd('startinsert') + end end end @@ -261,6 +274,9 @@ function Chat:finish(msg) end self:show_help(msg, -2) + if self.auto_insert and self:active() then + vim.cmd('startinsert') + end end return Chat diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 644c946d..48de280d 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -58,7 +58,6 @@ local select = require('CopilotChat.select') ---@field proxy string? ---@field allow_insecure boolean? ---@field system_prompt string? ----@field yank_diff_register string? ---@field model string? ---@field temperature number? ---@field question_header string? @@ -83,8 +82,6 @@ return { proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections - yank_diff_register = '"', -- Allows overriding the register for yanking diffs - system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o-2024-05-13', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or `gpt-4o-2024-05-13` temperature = 0.1, -- GPT temperature @@ -97,7 +94,8 @@ return { show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and if auto follow cursor is enabled on new prompt + auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + insert_at_end = false, -- Move cursor to end of buffer when inserting text clear_chat_on_new_prompt = false, -- Clears chat on every new prompt highlight_selection = true, -- Highlight selection @@ -197,7 +195,7 @@ return { zindex = 1, -- determines if window is on top or below other floating windows }, - -- default mappings (in tables first is normal mode, second is insert mode) + -- default mappings mappings = { complete = { detail = 'Use @ or / for options.', @@ -221,6 +219,7 @@ return { }, yank_diff = { normal = 'gy', + register = '"', }, show_diff = { normal = 'gd', diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7198c61c..98d69fc3 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -307,7 +307,7 @@ end --- Open the chat window. ---@param config CopilotChat.config|CopilotChat.config.prompt|nil ---@param source CopilotChat.config.source? -function M.open(config, source, no_insert) +function M.open(config, source) config = vim.tbl_deep_extend('force', M.config, config or {}) local should_reset = state.config and not utils.table_equals(config.window, state.config.window) state.config = config @@ -316,8 +316,7 @@ function M.open(config, source, no_insert) winnr = vim.api.nvim_get_current_win(), }) - vim.cmd('stopinsert') - utils.exit_visual_mode() + utils.return_to_normal_mode() -- Recreate the window if the layout has changed if should_reset then @@ -325,22 +324,12 @@ function M.open(config, source, no_insert) end state.chat:open(config) - state.chat:focus() state.chat:follow() - - if - not no_insert - and not state.copilot:running() - and M.config.auto_insert_mode - and state.chat:active() - then - vim.cmd('startinsert') - end + state.chat:focus() end --- Close the chat window. function M.close() - vim.cmd('stopinsert') state.chat:close(state.source and state.source.bufnr or nil) end @@ -387,10 +376,10 @@ function M.ask(prompt, config, source) return end - M.open(config, source, true) + M.open(config, source) if config.clear_chat_on_new_prompt then - M.stop(true, true) + M.stop(true) end state.last_system_prompt = system_prompt @@ -412,7 +401,6 @@ function M.ask(prompt, config, source) append(updated_prompt) append('\n\n' .. config.answer_header .. config.separator .. '\n\n') - state.chat:follow() local selected_context = config.context if string.find(prompt, '@buffers') then @@ -428,9 +416,6 @@ function M.ask(prompt, config, source) append('```\n' .. err .. '\n```') append('\n\n' .. config.question_header .. config.separator .. '\n\n') state.chat:finish() - if M.config.auto_follow_cursor and M.config.auto_insert_mode and state.chat:active() then - vim.cmd('startinsert') - end end) end @@ -466,9 +451,6 @@ function M.ask(prompt, config, source) if config.callback then config.callback(response, state.source) end - if config.auto_follow_cursor and config.auto_insert_mode and state.chat:active() then - vim.cmd('startinsert') - end end) end, on_progress = function(token) @@ -483,7 +465,7 @@ end --- Stop current copilot output and optionally reset the chat ten show the help message. ---@param reset boolean? -function M.stop(reset, no_insert) +function M.stop(reset) state.response = nil local stopped = reset and state.copilot:reset() or state.copilot:stop() local wrap = vim.schedule @@ -501,11 +483,6 @@ function M.stop(reset, no_insert) end append(M.config.question_header .. M.config.separator .. '\n\n') state.chat:finish() - state.chat:follow() - - if not no_insert and M.config.auto_insert_mode and state.chat:active() then - vim.cmd('startinsert') - end end) end @@ -589,19 +566,29 @@ end function M.setup(config) -- Handle old mapping format and show error local found_old_format = false - if config and config.mappings then - for name, key in pairs(config.mappings) do - if type(key) == 'string' then - vim.notify( - 'config.mappings.' - .. name - .. ": 'mappings' format have changed, please update your configuration, for now revering to default settings. See ':help CopilotChat-configuration' for current format", - vim.log.levels.ERROR - ) - found_old_format = true + if config then + if config.mappings then + for name, key in pairs(config.mappings) do + if type(key) == 'string' then + vim.notify( + 'config.mappings.' + .. name + .. ": 'mappings' format have changed, please update your configuration, for now revering to default settings. See ':help CopilotChat-configuration' for current format", + vim.log.levels.ERROR + ) + found_old_format = true + end end end + + if config.yank_diff_register then + vim.notify( + 'config.yank_diff_register: This option has been removed, please use mappings.yank_diff.register instead', + vim.log.levels.ERROR + ) + end end + if found_old_format then config.mappings = nil end @@ -722,7 +709,7 @@ function M.setup(config) state.chat:close(state.source and state.source.bufnr or nil) state.chat:delete() end - state.chat = Chat(chat_help, function(bufnr) + state.chat = Chat(chat_help, M.config.auto_insert_mode, function(bufnr) map_key(M.config.mappings.complete, bufnr, complete) map_key(M.config.mappings.reset, bufnr, M.reset) map_key(M.config.mappings.close, bufnr, M.close) @@ -775,7 +762,7 @@ function M.setup(config) local lines = find_lines_between_separator(section_lines, '^```%w*$', true) if #lines > 0 then local content = table.concat(lines, '\n') - vim.fn.setreg(M.config.yank_diff_register, content) + vim.fn.setreg(M.config.mappings.yank_diff.register, content) end end) @@ -841,6 +828,17 @@ function M.setup(config) end, }) + if M.config.insert_at_end then + vim.api.nvim_create_autocmd({ 'InsertEnter' }, { + buffer = state.chat.bufnr, + callback = function() + vim.cmd('normal! 0') + vim.cmd('normal! G$') + vim.v.char = 'x' + end, + }) + end + append(M.config.question_header .. M.config.separator .. '\n\n') state.chat:finish() end) diff --git a/lua/CopilotChat/integrations/fzflua.lua b/lua/CopilotChat/integrations/fzflua.lua index ff1f3cfd..2091e71b 100644 --- a/lua/CopilotChat/integrations/fzflua.lua +++ b/lua/CopilotChat/integrations/fzflua.lua @@ -12,7 +12,7 @@ function M.pick(pick_actions, opts) return end - utils.exit_visual_mode() + utils.return_to_normal_mode() opts = vim.tbl_extend('force', { prompt = pick_actions.prompt .. '> ', preview = fzflua.shell.raw_preview_action_cmd(function(items) diff --git a/lua/CopilotChat/integrations/telescope.lua b/lua/CopilotChat/integrations/telescope.lua index 0d733053..e0c4dcee 100644 --- a/lua/CopilotChat/integrations/telescope.lua +++ b/lua/CopilotChat/integrations/telescope.lua @@ -18,7 +18,7 @@ function M.pick(pick_actions, opts) return end - utils.exit_visual_mode() + utils.return_to_normal_mode() opts = themes.get_dropdown(opts or {}) pickers .new(opts, { diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 8759499a..7074b806 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -100,11 +100,14 @@ function M.table_equals(a, b) return true end ---- Exit visual mode if we are in it -function M.exit_visual_mode() - if vim.fn.mode():lower():find('v') then +--- Return to normal mode +function M.return_to_normal_mode() + local mode = vim.fn.mode():lower() + if mode:find('v') then -- NOTE: vim.cmd('normal! v') does not work properly when executed from keymap vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, false, true), 'x', false) + elseif mode:find('i') then + vim.cmd('stopinsert') end end From e2ce8d61737172aaa9b0acf1eeb9a1fc966c7f08 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 31 Aug 2024 00:59:15 +0000 Subject: [PATCH 038/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 73d61ff3..280b0165 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 22 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 31 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -230,8 +230,6 @@ Also see here : proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections - yank_diff_register = '"', -- Allow overriding the register for yanking diffs - system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or 'gpt-4o' temperature = 0.1, -- GPT temperature @@ -244,7 +242,8 @@ Also see here : show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and if auto follow cursor is enabled on new prompt + auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + insert_at_end = false, -- Move cursor to end of buffer when inserting text clear_chat_on_new_prompt = false, -- Clears chat on every new prompt highlight_selection = true, -- Highlight selection in the source buffer when in the chat window @@ -335,6 +334,7 @@ Also see here : }, yank_diff = { normal = 'gy', + register = '"', }, show_diff = { normal = 'gd' From b4e4adb46f56fb9cc639bab0bb7a30e1ada4441a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 31 Aug 2024 03:37:14 +0200 Subject: [PATCH 039/338] Use cwd for selection.gitdiff This fixes issue with switching directories after opening neovim. Closes #387 Signed-off-by: Tomas Slusny --- lua/CopilotChat/select.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index e21ea69f..96a07e97 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -161,7 +161,8 @@ function M.gitdiff(source, staged) return nil end - local cmd = 'git diff --no-color --no-ext-diff' .. (staged and ' --staged' or '') + local dir = vim.fn.getcwd() + local cmd = 'git -C ' .. dir .. ' diff --no-color --no-ext-diff' .. (staged and ' --staged' or '') local handle = io.popen(cmd) if not handle then return nil From ce68b1f5f3fe1aa7e27eccf19366f2a8ba94faac Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 31 Aug 2024 03:26:50 +0200 Subject: [PATCH 040/338] Move help to separate window Closes #361 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 4 + lua/CopilotChat/init.lua | 292 +++++++++++++++++++------------------ 2 files changed, 157 insertions(+), 139 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 48de280d..9889001a 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -51,6 +51,7 @@ local select = require('CopilotChat.select') ---@field show_diff CopilotChat.config.mapping? ---@field show_system_prompt CopilotChat.config.mapping? ---@field show_user_selection CopilotChat.config.mapping? +---@field show_help CopilotChat.config.mapping? --- CopilotChat default configuration ---@class CopilotChat.config @@ -230,5 +231,8 @@ return { show_user_selection = { normal = 'gs', }, + show_help = { + normal = 'gh', + }, }, } diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 98d69fc3..9487f617 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -23,6 +23,7 @@ local plugin_name = 'CopilotChat.nvim' --- @field diff CopilotChat.Overlay? --- @field system_prompt CopilotChat.Overlay? --- @field user_selection CopilotChat.Overlay? +--- @field help CopilotChat.Overlay? local state = { copilot = nil, chat = nil, @@ -40,6 +41,7 @@ local state = { diff = nil, system_prompt = nil, user_selection = nil, + help = nil, } local function blend_color_with_neovim_bg(color_name, blend) @@ -213,9 +215,13 @@ end --- Get the info for a key. ---@param name string ----@param key CopilotChat.config.mapping +---@param key CopilotChat.config.mapping? ---@return string local function key_to_info(name, key) + if not key then + return '' + end + local out = '' if key.normal and key.normal ~= '' then out = out .. "'" .. key.normal .. "' in normal mode" @@ -625,14 +631,8 @@ function M.setup(config) { link = '@punctuation.special.markdown', default = true } ) - local overlay_help = '' - if M.config.mappings.close then - overlay_help = key_to_info('close', M.config.mappings.close) - end - local diff_help = '' - if M.config.mappings.accept_diff then - diff_help = key_to_info('accept_diff', M.config.mappings.accept_diff) - end + local overlay_help = key_to_info('close', M.config.mappings.close) + local diff_help = key_to_info('accept_diff', M.config.mappings.accept_diff) if overlay_help ~= '' and diff_help ~= '' then diff_help = diff_help .. '\n' .. overlay_help end @@ -688,160 +688,174 @@ function M.setup(config) end) end) - local chat_help = '' - if M.config.show_help then - local chat_keys = vim.tbl_keys(M.config.mappings) - table.sort(chat_keys, function(a, b) - a = M.config.mappings[a] - a = a.normal or a.insert - b = M.config.mappings[b] - b = b.normal or b.insert - return a < b - end) - - for _, name in ipairs(chat_keys) do - local key = M.config.mappings[name] - chat_help = chat_help .. key_to_info(name, key) .. '\n' - end + if state.help then + state.help:delete() end + state.help = Overlay('copilot-help', hl_ns, overlay_help, function(bufnr) + map_key(M.config.mappings.close, bufnr, function() + state.help:restore(state.chat.winnr, state.chat.bufnr) + end) + end) if state.chat then state.chat:close(state.source and state.source.bufnr or nil) state.chat:delete() end - state.chat = Chat(chat_help, M.config.auto_insert_mode, function(bufnr) - map_key(M.config.mappings.complete, bufnr, complete) - map_key(M.config.mappings.reset, bufnr, M.reset) - map_key(M.config.mappings.close, bufnr, M.close) - - map_key(M.config.mappings.submit_prompt, bufnr, function() - local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local lines, start_line, end_line, line_count = - find_lines_between_separator(chat_lines, M.config.separator .. '$') - local input = vim.trim(table.concat(lines, '\n')) - if input ~= '' then - -- If we are entering the input at the end, replace it - if line_count == end_line then - vim.api.nvim_buf_set_lines(bufnr, start_line, end_line, false, { '' }) + state.chat = Chat( + M.config.show_help and key_to_info('show_help', M.config.mappings.show_help), + M.config.auto_insert_mode, + function(bufnr) + map_key(M.config.mappings.show_help, bufnr, function() + local chat_help = '' + local chat_keys = vim.tbl_keys(M.config.mappings) + table.sort(chat_keys, function(a, b) + a = M.config.mappings[a] + a = a.normal or a.insert + b = M.config.mappings[b] + b = b.normal or b.insert + return a < b + end) + for _, name in ipairs(chat_keys) do + local key = M.config.mappings[name] + chat_help = chat_help .. key_to_info(name, key) .. '\n' end - M.ask(input, state.config, state.source) - end - end) - - map_key(M.config.mappings.accept_diff, bufnr, function() - local selection = get_selection() - if not selection.start_row or not selection.end_row then - return - end - - local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local section_lines = - find_lines_between_separator(chat_lines, M.config.separator .. '$', true) - local lines = find_lines_between_separator(section_lines, '^```%w*$', true) - if #lines > 0 then - vim.api.nvim_buf_set_text( - state.source.bufnr, - selection.start_row - 1, - selection.start_col - 1, - selection.end_row - 1, - selection.end_col, - lines - ) - end - end) + chat_help = chat_help .. M.config.separator .. '\n' + state.help:show(chat_help, 'markdown', 'markdown', state.chat.winnr) + end) - map_key(M.config.mappings.yank_diff, bufnr, function() - local selection = get_selection() - if not selection.start_row or not selection.end_row then - return - end + map_key(M.config.mappings.complete, bufnr, complete) + map_key(M.config.mappings.reset, bufnr, M.reset) + map_key(M.config.mappings.close, bufnr, M.close) + + map_key(M.config.mappings.submit_prompt, bufnr, function() + local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local lines, start_line, end_line, line_count = + find_lines_between_separator(chat_lines, M.config.separator .. '$') + local input = vim.trim(table.concat(lines, '\n')) + if input ~= '' then + -- If we are entering the input at the end, replace it + if line_count == end_line then + vim.api.nvim_buf_set_lines(bufnr, start_line, end_line, false, { '' }) + end + M.ask(input, state.config, state.source) + end + end) - local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local section_lines = - find_lines_between_separator(chat_lines, M.config.separator .. '$', true) - local lines = find_lines_between_separator(section_lines, '^```%w*$', true) - if #lines > 0 then - local content = table.concat(lines, '\n') - vim.fn.setreg(M.config.mappings.yank_diff.register, content) - end - end) + map_key(M.config.mappings.accept_diff, bufnr, function() + local selection = get_selection() + if not selection.start_row or not selection.end_row then + return + end - map_key(M.config.mappings.show_diff, bufnr, function() - local selection = get_selection() - if not selection or not selection.start_row or not selection.end_row then - return - end + local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local section_lines = + find_lines_between_separator(chat_lines, M.config.separator .. '$', true) + local lines = find_lines_between_separator(section_lines, '^```%w*$', true) + if #lines > 0 then + vim.api.nvim_buf_set_text( + state.source.bufnr, + selection.start_row - 1, + selection.start_col - 1, + selection.end_row - 1, + selection.end_col, + lines + ) + end + end) - local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) - local section_lines = - find_lines_between_separator(chat_lines, M.config.separator .. '$', true) - local lines = - table.concat(find_lines_between_separator(section_lines, '^```%w*$', true), '\n') - if vim.trim(lines) ~= '' then - state.last_code_output = lines + map_key(M.config.mappings.yank_diff, bufnr, function() + local selection = get_selection() + if not selection.start_row or not selection.end_row then + return + end - local filetype = selection.filetype or vim.bo[state.source.bufnr].filetype + local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local section_lines = + find_lines_between_separator(chat_lines, M.config.separator .. '$', true) + local lines = find_lines_between_separator(section_lines, '^```%w*$', true) + if #lines > 0 then + local content = table.concat(lines, '\n') + vim.fn.setreg(M.config.mappings.yank_diff.register, content) + end + end) - local diff = tostring(vim.diff(selection.lines, lines, { - result_type = 'unified', - ignore_blank_lines = true, - ignore_whitespace = true, - ignore_whitespace_change = true, - ignore_whitespace_change_at_eol = true, - ignore_cr_at_eol = true, - algorithm = 'myers', - ctxlen = #selection.lines, - })) - - state.diff:show(diff, filetype, 'diff', state.chat.winnr) - end - end) + map_key(M.config.mappings.show_diff, bufnr, function() + local selection = get_selection() + if not selection or not selection.start_row or not selection.end_row then + return + end - map_key(M.config.mappings.show_system_prompt, bufnr, function() - local prompt = state.last_system_prompt or M.config.system_prompt - if not prompt then - return - end + local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) + local section_lines = + find_lines_between_separator(chat_lines, M.config.separator .. '$', true) + local lines = + table.concat(find_lines_between_separator(section_lines, '^```%w*$', true), '\n') + if vim.trim(lines) ~= '' then + state.last_code_output = lines + + local filetype = selection.filetype or vim.bo[state.source.bufnr].filetype + + local diff = tostring(vim.diff(selection.lines, lines, { + result_type = 'unified', + ignore_blank_lines = true, + ignore_whitespace = true, + ignore_whitespace_change = true, + ignore_whitespace_change_at_eol = true, + ignore_cr_at_eol = true, + algorithm = 'myers', + ctxlen = #selection.lines, + })) + + state.diff:show(diff, filetype, 'diff', state.chat.winnr) + end + end) - state.system_prompt:show(prompt, 'markdown', 'markdown', state.chat.winnr) - end) + map_key(M.config.mappings.show_system_prompt, bufnr, function() + local prompt = state.last_system_prompt or M.config.system_prompt + if not prompt then + return + end - map_key(M.config.mappings.show_user_selection, bufnr, function() - local selection = get_selection() - if not selection.start_row or not selection.end_row then - return - end + state.system_prompt:show(prompt, 'markdown', 'markdown', state.chat.winnr) + end) - local filetype = selection.filetype or vim.bo[state.source.bufnr].filetype - local lines = selection.lines - if vim.trim(lines) ~= '' then - state.user_selection:show(lines, filetype, filetype, state.chat.winnr) - end - end) + map_key(M.config.mappings.show_user_selection, bufnr, function() + local selection = get_selection() + if not selection.start_row or not selection.end_row then + return + end - vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { - buffer = state.chat.bufnr, - callback = function(ev) - if state.config.highlight_selection then - M.highlight_selection(ev.event == 'BufLeave') + local filetype = selection.filetype or vim.bo[state.source.bufnr].filetype + local lines = selection.lines + if vim.trim(lines) ~= '' then + state.user_selection:show(lines, filetype, filetype, state.chat.winnr) end - end, - }) + end) - if M.config.insert_at_end then - vim.api.nvim_create_autocmd({ 'InsertEnter' }, { + vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { buffer = state.chat.bufnr, - callback = function() - vim.cmd('normal! 0') - vim.cmd('normal! G$') - vim.v.char = 'x' + callback = function(ev) + if state.config.highlight_selection then + M.highlight_selection(ev.event == 'BufLeave') + end end, }) - end - append(M.config.question_header .. M.config.separator .. '\n\n') - state.chat:finish() - end) + if M.config.insert_at_end then + vim.api.nvim_create_autocmd({ 'InsertEnter' }, { + buffer = state.chat.bufnr, + callback = function() + vim.cmd('normal! 0') + vim.cmd('normal! G$') + vim.v.char = 'x' + end, + }) + end + + append(M.config.question_header .. M.config.separator .. '\n\n') + state.chat:finish() + end + ) for name, prompt in pairs(M.prompts(true)) do vim.api.nvim_create_user_command('CopilotChat' .. name, function(args) From b8d713a0b6179448c05bfa8eb25826ba0c71256d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 31 Aug 2024 18:19:14 +0200 Subject: [PATCH 041/338] Properly resolve directory based on source buffer name getcwd is not reliable and straight up doesnt work even when supplying correct window number as first parameter so use filename instead Closes #387 Signed-off-by: Tomas Slusny --- lua/CopilotChat/select.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 96a07e97..5ba7a08e 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -161,7 +161,13 @@ function M.gitdiff(source, staged) return nil end - local dir = vim.fn.getcwd() + local bufname = vim.api.nvim_buf_get_name(source.bufnr) + local file_path = bufname:gsub('^%w+://', '') + local dir = vim.fn.fnamemodify(file_path, ':h') + if not dir or dir == '' then + return nil + end + local cmd = 'git -C ' .. dir .. ' diff --no-color --no-ext-diff' .. (staged and ' --staged' or '') local handle = io.popen(cmd) if not handle then @@ -170,7 +176,6 @@ function M.gitdiff(source, staged) local result = handle:read('*a') handle:close() - if not result or result == '' then return nil end From 50a9877eafb15a6861fe59a11734595b53d14f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Renzo=20Mondrag=C3=B3n?= Date: Fri, 6 Sep 2024 05:52:46 -0500 Subject: [PATCH 042/338] Fix typo in Optimize prompt message (#404) * fix(prompt): correct typo in Optimize prompt message * fix: correct spelling of 'readability' in multiple files --- README.md | 4 ++-- cspell-tool.txt | 2 +- doc/CopilotChat.txt | 4 ++-- lua/CopilotChat/config.lua | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 960fdb2b..f01eb3f9 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/ma - `:CopilotChatExplain` - Write an explanation for the active selection as paragraphs of text - `:CopilotChatReview` - Review the selected code - `:CopilotChatFix` - There is a problem in this code. Rewrite the code to show it with the bug fixed -- `:CopilotChatOptimize` - Optimize the selected code to improve performance and readablilty +- `:CopilotChatOptimize` - Optimize the selected code to improve performance and readability - `:CopilotChatDocs` - Please add documentation comment for the selection - `:CopilotChatTests` - Please generate tests for my code - `:CopilotChatFixDiagnostic` - Please assist with the following diagnostic issue in file @@ -242,7 +242,7 @@ Also see [here](/lua/CopilotChat/config.lua): prompt = '/COPILOT_GENERATE There is a problem in this code. Rewrite the code to show it with the bug fixed.', }, Optimize = { - prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readablilty.', + prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readability.', }, Docs = { prompt = '/COPILOT_GENERATE Please add documentation comment for the selection.', diff --git a/cspell-tool.txt b/cspell-tool.txt index d5dd381e..52b182ed 100644 --- a/cspell-tool.txt +++ b/cspell-tool.txt @@ -52,7 +52,7 @@ zbirenbaum nnoremap treyhunner nekowasabi -readablilty +readability deathbeam jellydn's gptlang diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 280b0165..c0a2fc75 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -142,7 +142,7 @@ COMMANDS COMING FROM DEFAULT PROMPTS - `:CopilotChatExplain` - Write an explanation for the active selection as paragraphs of text - `:CopilotChatReview` - Review the selected code - `:CopilotChatFix` - There is a problem in this code. Rewrite the code to show it with the bug fixed -- `:CopilotChatOptimize` - Optimize the selected code to improve performance and readablilty +- `:CopilotChatOptimize` - Optimize the selected code to improve performance and readability - `:CopilotChatDocs` - Please add documentation comment for the selection - `:CopilotChatTests` - Please generate tests for my code - `:CopilotChatFixDiagnostic` - Please assist with the following diagnostic issue in file @@ -271,7 +271,7 @@ Also see here : prompt = '/COPILOT_GENERATE There is a problem in this code. Rewrite the code to show it with the bug fixed.', }, Optimize = { - prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readablilty.', + prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readability.', }, Docs = { prompt = '/COPILOT_GENERATE Please add documentation comment for the selection.', diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 9889001a..9093a8ab 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -157,7 +157,7 @@ return { prompt = '/COPILOT_GENERATE There is a problem in this code. Rewrite the code to show it with the bug fixed.', }, Optimize = { - prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readablilty.', + prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readability.', }, Docs = { prompt = '/COPILOT_GENERATE Please add documentation comment for the selection.', From 6cf83b752ad8834d3ad6f11be87bdebf43883833 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Sep 2024 10:53:08 +0000 Subject: [PATCH 043/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index c0a2fc75..ca775679 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 August 31 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 06 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 9e7010bd33808e31d3f729b5e18a772d8e84f704 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:53:33 +0200 Subject: [PATCH 044/338] docs: add renxzen as a contributor for code, and doc (#405) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 151 ++++++++++++++++++++++++++++++++++---------- README.md | 5 +- 2 files changed, 120 insertions(+), 36 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 68197c9f..4ca1ef2c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,5 +1,7 @@ { - "files": ["README.md"], + "files": [ + "README.md" + ], "imageSize": 100, "commit": false, "commitType": "docs", @@ -10,224 +12,307 @@ "name": "gptlang", "avatar_url": "https://avatars.githubusercontent.com/u/121417512?v=4", "profile": "https://github.com/gptlang", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "jellydn", "name": "Dung Duc Huynh (Kaka)", "avatar_url": "https://avatars.githubusercontent.com/u/870029?v=4", "profile": "https://productsway.com/", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "qoobes", "name": "Ahmed Haracic", "avatar_url": "https://avatars.githubusercontent.com/u/58834655?v=4", "profile": "https://qoobes.dev", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "ziontee113", "name": "Trí Thiện Nguyễn", "avatar_url": "https://avatars.githubusercontent.com/u/102876811?v=4", "profile": "https://youtube.com/@ziontee113", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "Cassius0924", "name": "He Zhizhou", "avatar_url": "https://avatars.githubusercontent.com/u/62874592?v=4", "profile": "https://github.com/Cassius0924", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "rguruprakash", "name": "Guruprakash Rajakkannu", "avatar_url": "https://avatars.githubusercontent.com/u/9963717?v=4", "profile": "https://www.linkedin.com/in/guruprakashrajakkannu/", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "kristofka", "name": "kristofka", "avatar_url": "https://avatars.githubusercontent.com/u/140354?v=4", "profile": "https://github.com/kristofka", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "PostCyberPunk", "name": "PostCyberPunk", "avatar_url": "https://avatars.githubusercontent.com/u/134976996?v=4", "profile": "https://github.com/PostCyberPunk", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "ktns", "name": "Katsuhiko Nishimra", "avatar_url": "https://avatars.githubusercontent.com/u/1302759?v=4", "profile": "https://github.com/ktns", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "errnoh", "name": "Erno Hopearuoho", "avatar_url": "https://avatars.githubusercontent.com/u/373946?v=4", "profile": "https://github.com/errnoh", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "shaungarwood", "name": "Shaun Garwood", "avatar_url": "https://avatars.githubusercontent.com/u/4156525?v=4", "profile": "https://github.com/shaungarwood", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "neutrinoA4", "name": "neutrinoA4", "avatar_url": "https://avatars.githubusercontent.com/u/122616073?v=4", "profile": "https://github.com/neutrinoA4", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "banjocat", "name": "Jack Muratore", "avatar_url": "https://avatars.githubusercontent.com/u/3247309?v=4", "profile": "https://github.com/banjocat", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "AdrielVelazquez", "name": "Adriel Velazquez", "avatar_url": "https://avatars.githubusercontent.com/u/3443378?v=4", "profile": "https://github.com/AdrielVelazquez", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "deathbeam", "name": "Tomas Slusny", "avatar_url": "https://avatars.githubusercontent.com/u/5115805?v=4", "profile": "https://github.com/deathbeam", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "nisalVD", "name": "Nisal", "avatar_url": "https://avatars.githubusercontent.com/u/30633436?v=4", "profile": "http://nisalvd.netlify.com/", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "gaardhus", "name": "Tobias Gårdhus", "avatar_url": "https://avatars.githubusercontent.com/u/46934916?v=4", "profile": "http://www.gaardhus.dk", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "PetrDlouhy", "name": "Petr Dlouhý", "avatar_url": "https://avatars.githubusercontent.com/u/156755?v=4", "profile": "https://www.patreon.com/PetrDlouhy", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "dmadisetti", "name": "Dylan Madisetti", "avatar_url": "https://avatars.githubusercontent.com/u/2689338?v=4", "profile": "http://www.dylanmadisetti.com", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "aweis89", "name": "Aaron Weisberg", "avatar_url": "https://avatars.githubusercontent.com/u/5186956?v=4", "profile": "https://github.com/aweis89", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "tlacuilose", "name": "Jose Tlacuilo", "avatar_url": "https://avatars.githubusercontent.com/u/65783495?v=4", "profile": "https://github.com/tlacuilose", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "kevintraver", "name": "Kevin Traver", "avatar_url": "https://avatars.githubusercontent.com/u/196406?v=4", "profile": "http://kevintraver.com", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "D7ry", "name": "dTry", "avatar_url": "https://avatars.githubusercontent.com/u/92609548?v=4", "profile": "https://github.com/D7ry", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "ornew", "name": "Arata Furukawa", "avatar_url": "https://avatars.githubusercontent.com/u/19766770?v=4", "profile": "https://blog.ornew.io", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "lingjie00", "name": "Ling", "avatar_url": "https://avatars.githubusercontent.com/u/64540764?v=4", "profile": "https://github.com/lingjie00", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "frolvanya", "name": "Ivan Frolov", "avatar_url": "https://avatars.githubusercontent.com/u/59515280?v=4", "profile": "https://github.com/frolvanya", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "folke", "name": "Folke Lemaitre", "avatar_url": "https://avatars.githubusercontent.com/u/292349?v=4", "profile": "http://www.folkelemaitre.com", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "GitMurf", "name": "GitMurf", "avatar_url": "https://avatars.githubusercontent.com/u/64155612?v=4", "profile": "https://github.com/GitMurf", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "festeh", "name": "Dmitrii Lipin", "avatar_url": "https://avatars.githubusercontent.com/u/6877858?v=4", "profile": "http://dimalip.in", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "jinzhongjia", "name": "jinzhongjia", "avatar_url": "https://avatars.githubusercontent.com/u/41784264?v=4", "profile": "https://nvimer.org", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "guill", "name": "guill", "avatar_url": "https://avatars.githubusercontent.com/u/3157454?v=4", "profile": "https://github.com/guill", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "sjonpaulbrown-cc", "name": "Sjon-Paul Brown", "avatar_url": "https://avatars.githubusercontent.com/u/81941908?v=4", "profile": "https://github.com/sjonpaulbrown-cc", - "contributions": ["code"] + "contributions": [ + "code" + ] + }, + { + "login": "renxzen", + "name": "Renzo Mondragón", + "avatar_url": "https://avatars.githubusercontent.com/u/13023797?v=4", + "profile": "https://github.com/renxzen", + "contributions": [ + "code", + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index f01eb3f9..553a08cd 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,7 @@ [![Discord](https://img.shields.io/discord/1200633211236122665.svg)](https://discord.gg/vy6hJsTWaZ) - -[![All Contributors](https://img.shields.io/badge/all_contributors-32-orange.svg?style=flat-square)](#contributors-) - +[![All Contributors](https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square)](#contributors-) > [!NOTE] @@ -606,6 +604,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d jinzhongjia
jinzhongjia

📖 guill
guill

💻 Sjon-Paul Brown
Sjon-Paul Brown

💻 + Renzo Mondragón
Renzo Mondragón

💻 📖 From ba7e78d731bad3d1f629946c62c9d76635a808f0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 12 Sep 2024 13:39:37 +0200 Subject: [PATCH 045/338] fix: Properly load auto follow cursor config when appending text Closes #317 Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 12 +++++--- lua/CopilotChat/init.lua | 59 +++++++++++++++++++--------------------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 9a494c0f..078cf516 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -148,15 +148,19 @@ end function Chat:open(config) self:validate() - if self:visible() then - return - end - local window = config.window local layout = window.layout local width = window.width > 1 and window.width or math.floor(vim.o.columns * window.width) local height = window.height > 1 and window.height or math.floor(vim.o.lines * window.height) + if self.layout ~= layout then + self:close() + end + + if self:visible() then + return + end + if layout == 'float' then local win_opts = { style = 'minimal', diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9487f617..853902d3 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -124,9 +124,10 @@ end --- Append a string to the chat window. ---@param str (string) -local function append(str) +---@param config CopilotChat.config +local function append(str, config) state.chat:append(str) - if M.config.auto_follow_cursor then + if config.auto_follow_cursor then state.chat:follow() end end @@ -315,7 +316,6 @@ end ---@param source CopilotChat.config.source? function M.open(config, source) config = vim.tbl_deep_extend('force', M.config, config or {}) - local should_reset = state.config and not utils.table_equals(config.window, state.config.window) state.config = config state.source = vim.tbl_extend('keep', source or {}, { bufnr = vim.api.nvim_get_current_buf(), @@ -323,12 +323,6 @@ function M.open(config, source) }) utils.return_to_normal_mode() - - -- Recreate the window if the layout has changed - if should_reset then - M.close() - end - state.chat:open(config) state.chat:follow() state.chat:focus() @@ -385,7 +379,7 @@ function M.ask(prompt, config, source) M.open(config, source) if config.clear_chat_on_new_prompt then - M.stop(true) + M.stop(true, config) end state.last_system_prompt = system_prompt @@ -402,11 +396,11 @@ function M.ask(prompt, config, source) end if state.copilot:stop() then - append('\n\n' .. config.question_header .. config.separator .. '\n\n') + append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) end - append(updated_prompt) - append('\n\n' .. config.answer_header .. config.separator .. '\n\n') + append(updated_prompt, config) + append('\n\n' .. config.answer_header .. config.separator .. '\n\n', config) local selected_context = config.context if string.find(prompt, '@buffers') then @@ -418,9 +412,9 @@ function M.ask(prompt, config, source) local function on_error(err) vim.schedule(function() - append('\n\n' .. config.error_header .. config.separator .. '\n\n') - append('```\n' .. err .. '\n```') - append('\n\n' .. config.question_header .. config.separator .. '\n\n') + append('\n\n' .. config.error_header .. config.separator .. '\n\n', config) + append('```\n' .. err .. '\n```', config) + append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) state.chat:finish() end) end @@ -447,7 +441,7 @@ function M.ask(prompt, config, source) on_error = on_error, on_done = function(response, token_count) vim.schedule(function() - append('\n\n' .. config.question_header .. config.separator .. '\n\n') + append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) state.response = response if tiktoken.available() and token_count and token_count > 0 then state.chat:finish(token_count .. ' tokens used') @@ -461,7 +455,7 @@ function M.ask(prompt, config, source) end, on_progress = function(token) vim.schedule(function() - append(token) + append(token, config) end) end, }) @@ -471,7 +465,9 @@ end --- Stop current copilot output and optionally reset the chat ten show the help message. ---@param reset boolean? -function M.stop(reset) +---@param config CopilotChat.config? +function M.stop(reset, config) + config = vim.tbl_deep_extend('force', M.config, config or {}) state.response = nil local stopped = reset and state.copilot:reset() or state.copilot:stop() local wrap = vim.schedule @@ -485,16 +481,17 @@ function M.stop(reset) if reset then state.chat:clear() else - append('\n\n') + append('\n\n', config) end - append(M.config.question_header .. M.config.separator .. '\n\n') + append(M.config.question_header .. M.config.separator .. '\n\n', config) state.chat:finish() end) end --- Reset the chat window and show the help message. -function M.reset() - M.stop(true) +---@param config CopilotChat.config? +function M.reset(config) + M.stop(true, config) end --- Save the chat history to a file. @@ -535,20 +532,20 @@ function M.load(name, history_path) for i, message in ipairs(history) do if message.role == 'user' then if i > 1 then - append('\n\n') + append('\n\n', state.config) end - append(M.config.question_header .. M.config.separator .. '\n\n') - append(message.content) + append(M.config.question_header .. M.config.separator .. '\n\n', state.config) + append(message.content, state.config) elseif message.role == 'assistant' then - append('\n\n' .. M.config.answer_header .. M.config.separator .. '\n\n') - append(message.content) + append('\n\n' .. M.config.answer_header .. M.config.separator .. '\n\n', state.config) + append(message.content, state.config) end end if #history > 0 then - append('\n\n') + append('\n\n', state.config) end - append(M.config.question_header .. M.config.separator .. '\n\n') + append(M.config.question_header .. M.config.separator .. '\n\n', state.config) state.chat:finish() M.open() @@ -852,7 +849,7 @@ function M.setup(config) }) end - append(M.config.question_header .. M.config.separator .. '\n\n') + append(M.config.question_header .. M.config.separator .. '\n\n', M.config) state.chat:finish() end ) From bc04833c61c1f6248c3258f2bac3ea4c7c61e2b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:40:28 +0000 Subject: [PATCH 046/338] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .all-contributorsrc | 146 +++++++++++--------------------------------- README.md | 2 + 2 files changed, 36 insertions(+), 112 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4ca1ef2c..d6f0f3db 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,7 +1,5 @@ { - "files": [ - "README.md" - ], + "files": ["README.md"], "imageSize": 100, "commit": false, "commitType": "docs", @@ -12,307 +10,231 @@ "name": "gptlang", "avatar_url": "https://avatars.githubusercontent.com/u/121417512?v=4", "profile": "https://github.com/gptlang", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "jellydn", "name": "Dung Duc Huynh (Kaka)", "avatar_url": "https://avatars.githubusercontent.com/u/870029?v=4", "profile": "https://productsway.com/", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "qoobes", "name": "Ahmed Haracic", "avatar_url": "https://avatars.githubusercontent.com/u/58834655?v=4", "profile": "https://qoobes.dev", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "ziontee113", "name": "Trí Thiện Nguyễn", "avatar_url": "https://avatars.githubusercontent.com/u/102876811?v=4", "profile": "https://youtube.com/@ziontee113", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "Cassius0924", "name": "He Zhizhou", "avatar_url": "https://avatars.githubusercontent.com/u/62874592?v=4", "profile": "https://github.com/Cassius0924", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "rguruprakash", "name": "Guruprakash Rajakkannu", "avatar_url": "https://avatars.githubusercontent.com/u/9963717?v=4", "profile": "https://www.linkedin.com/in/guruprakashrajakkannu/", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "kristofka", "name": "kristofka", "avatar_url": "https://avatars.githubusercontent.com/u/140354?v=4", "profile": "https://github.com/kristofka", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "PostCyberPunk", "name": "PostCyberPunk", "avatar_url": "https://avatars.githubusercontent.com/u/134976996?v=4", "profile": "https://github.com/PostCyberPunk", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "ktns", "name": "Katsuhiko Nishimra", "avatar_url": "https://avatars.githubusercontent.com/u/1302759?v=4", "profile": "https://github.com/ktns", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "errnoh", "name": "Erno Hopearuoho", "avatar_url": "https://avatars.githubusercontent.com/u/373946?v=4", "profile": "https://github.com/errnoh", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "shaungarwood", "name": "Shaun Garwood", "avatar_url": "https://avatars.githubusercontent.com/u/4156525?v=4", "profile": "https://github.com/shaungarwood", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "neutrinoA4", "name": "neutrinoA4", "avatar_url": "https://avatars.githubusercontent.com/u/122616073?v=4", "profile": "https://github.com/neutrinoA4", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "banjocat", "name": "Jack Muratore", "avatar_url": "https://avatars.githubusercontent.com/u/3247309?v=4", "profile": "https://github.com/banjocat", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "AdrielVelazquez", "name": "Adriel Velazquez", "avatar_url": "https://avatars.githubusercontent.com/u/3443378?v=4", "profile": "https://github.com/AdrielVelazquez", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "deathbeam", "name": "Tomas Slusny", "avatar_url": "https://avatars.githubusercontent.com/u/5115805?v=4", "profile": "https://github.com/deathbeam", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "nisalVD", "name": "Nisal", "avatar_url": "https://avatars.githubusercontent.com/u/30633436?v=4", "profile": "http://nisalvd.netlify.com/", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "gaardhus", "name": "Tobias Gårdhus", "avatar_url": "https://avatars.githubusercontent.com/u/46934916?v=4", "profile": "http://www.gaardhus.dk", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "PetrDlouhy", "name": "Petr Dlouhý", "avatar_url": "https://avatars.githubusercontent.com/u/156755?v=4", "profile": "https://www.patreon.com/PetrDlouhy", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "dmadisetti", "name": "Dylan Madisetti", "avatar_url": "https://avatars.githubusercontent.com/u/2689338?v=4", "profile": "http://www.dylanmadisetti.com", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "aweis89", "name": "Aaron Weisberg", "avatar_url": "https://avatars.githubusercontent.com/u/5186956?v=4", "profile": "https://github.com/aweis89", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "tlacuilose", "name": "Jose Tlacuilo", "avatar_url": "https://avatars.githubusercontent.com/u/65783495?v=4", "profile": "https://github.com/tlacuilose", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "kevintraver", "name": "Kevin Traver", "avatar_url": "https://avatars.githubusercontent.com/u/196406?v=4", "profile": "http://kevintraver.com", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "D7ry", "name": "dTry", "avatar_url": "https://avatars.githubusercontent.com/u/92609548?v=4", "profile": "https://github.com/D7ry", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "ornew", "name": "Arata Furukawa", "avatar_url": "https://avatars.githubusercontent.com/u/19766770?v=4", "profile": "https://blog.ornew.io", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "lingjie00", "name": "Ling", "avatar_url": "https://avatars.githubusercontent.com/u/64540764?v=4", "profile": "https://github.com/lingjie00", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "frolvanya", "name": "Ivan Frolov", "avatar_url": "https://avatars.githubusercontent.com/u/59515280?v=4", "profile": "https://github.com/frolvanya", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "folke", "name": "Folke Lemaitre", "avatar_url": "https://avatars.githubusercontent.com/u/292349?v=4", "profile": "http://www.folkelemaitre.com", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "GitMurf", "name": "GitMurf", "avatar_url": "https://avatars.githubusercontent.com/u/64155612?v=4", "profile": "https://github.com/GitMurf", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "festeh", "name": "Dmitrii Lipin", "avatar_url": "https://avatars.githubusercontent.com/u/6877858?v=4", "profile": "http://dimalip.in", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "jinzhongjia", "name": "jinzhongjia", "avatar_url": "https://avatars.githubusercontent.com/u/41784264?v=4", "profile": "https://nvimer.org", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "guill", "name": "guill", "avatar_url": "https://avatars.githubusercontent.com/u/3157454?v=4", "profile": "https://github.com/guill", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "sjonpaulbrown-cc", "name": "Sjon-Paul Brown", "avatar_url": "https://avatars.githubusercontent.com/u/81941908?v=4", "profile": "https://github.com/sjonpaulbrown-cc", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "renxzen", "name": "Renzo Mondragón", "avatar_url": "https://avatars.githubusercontent.com/u/13023797?v=4", "profile": "https://github.com/renxzen", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 553a08cd..06d810e2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![Discord](https://img.shields.io/discord/1200633211236122665.svg)](https://discord.gg/vy6hJsTWaZ) + [![All Contributors](https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square)](#contributors-) + > [!NOTE] From d43fab67c328946fbf8e24fdcadfdb5410517e1f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Sep 2024 08:58:37 +0000 Subject: [PATCH 047/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ca775679..76321442 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 06 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 13 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -591,7 +591,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -606,7 +606,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-32-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From 5458a86bd38d45d3570d081b740f7a724e29dfb3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 16 Sep 2024 01:04:46 +0200 Subject: [PATCH 048/338] fix: Use $LOCALAPPDATA when loading github token Also use $HOME instead of ~ as its more friendly for expansion on different OS. Fixes #409 Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 61301142..4e6ddfce 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -79,16 +79,17 @@ local function find_config_path() local config = vim.fn.expand('$XDG_CONFIG_HOME') if config and vim.fn.isdirectory(config) > 0 then return config - elseif vim.fn.has('win32') > 0 then - config = vim.fn.expand('~/AppData/Local') - if vim.fn.isdirectory(config) > 0 then - return config + end + if vim.fn.has('win32') > 0 then + config = vim.fn.expand('$LOCALAPPDATA') + if not config or vim.fn.isdirectory(config) == 0 then + config = vim.fn.expand('$HOME/AppData/Local') end else - config = vim.fn.expand('~/.config') - if vim.fn.isdirectory(config) > 0 then - return config - end + config = vim.fn.expand('$HOME/.config') + end + if config and vim.fn.isdirectory(config) > 0 then + return config end end From b70d043e8ddf8e67f9308c0f444223a0708d985c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Sep 2024 07:01:44 +0000 Subject: [PATCH 049/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 76321442..4125f873 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 13 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 16 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 0c9d886265487621d4590273627bb12922a8b27f Mon Sep 17 00:00:00 2001 From: fjchen7 Date: Tue, 17 Sep 2024 23:33:59 +0800 Subject: [PATCH 050/338] Fix indexing nil 'config' --- lua/CopilotChat/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 853902d3..f3e8cced 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -127,7 +127,7 @@ end ---@param config CopilotChat.config local function append(str, config) state.chat:append(str) - if config.auto_follow_cursor then + if config and config.auto_follow_cursor then state.chat:follow() end end From 36e11378c7ec75d459d57bcc556af118fc105f9e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 17 Sep 2024 16:53:55 +0000 Subject: [PATCH 051/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 4125f873..9633023b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 16 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 17 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 2352cd3e7e980cd73594be05f96b2dc4c0dd4a74 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:54:18 +0200 Subject: [PATCH 052/338] docs: add fjchen7 as a contributor for code (#414) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d6f0f3db..3cd28ec7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -235,6 +235,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/13023797?v=4", "profile": "https://github.com/renxzen", "contributions": ["code", "doc"] + }, + { + "login": "fjchen7", + "name": "fjchen7", + "avatar_url": "https://avatars.githubusercontent.com/u/10106636?v=4", + "profile": "https://github.com/fjchen7", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 06d810e2..8f355b91 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-) @@ -607,6 +607,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d guill
guill

💻 Sjon-Paul Brown
Sjon-Paul Brown

💻 Renzo Mondragón
Renzo Mondragón

💻 📖 + fjchen7
fjchen7

💻 From 849900e088631456bcb551a457de92b1fb4915f2 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 25 Sep 2024 12:30:32 +0200 Subject: [PATCH 053/338] Do not run git diff against .git directory This happens when running diff from `git commit` message. Closes #417 Signed-off-by: Tomas Slusny --- lua/CopilotChat/select.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 5ba7a08e..7d42df7b 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -167,6 +167,7 @@ function M.gitdiff(source, staged) if not dir or dir == '' then return nil end + dir = dir:gsub('.git$', '') local cmd = 'git -C ' .. dir .. ' diff --no-color --no-ext-diff' .. (staged and ' --staged' or '') local handle = io.popen(cmd) From 1acb735fd5d705df27ae1bad603160c5df7946c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Sep 2024 10:32:55 +0000 Subject: [PATCH 054/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9633023b..8a191f2e 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 17 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 25 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -591,7 +591,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -606,7 +606,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From 9333944fde3c65868818e245c73aa29eef826e9b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 25 Sep 2024 13:10:04 +0200 Subject: [PATCH 055/338] Null-check model choice after :CopilotChatModel If you dont select anything and just ESC it sets the model to nil atm. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f3e8cced..c5d6c2ae 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -356,7 +356,9 @@ function M.select_model() vim.ui.select(models, { prompt = 'Select a model', }, function(choice) - M.config.model = choice + if choice then + M.config.model = choice + end end) end) end) From 862e0313e37b5c09abcab8caeab8d499c7196941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Wo=C5=BAniak?= <184065+radwo@users.noreply.github.com> Date: Sat, 5 Oct 2024 20:54:22 +0200 Subject: [PATCH 056/338] refactor: migrate deprecated get_line_diagnostics to vim.diagnostic.get Replaced `vim.lsp.diagnostic.get_line_diagnostics` with `vim.diagnostic.get` to align with the updated Neovim API. --- lua/CopilotChat/actions.lua | 2 +- lua/CopilotChat/select.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/actions.lua b/lua/CopilotChat/actions.lua index f321ea0d..763cf8d1 100644 --- a/lua/CopilotChat/actions.lua +++ b/lua/CopilotChat/actions.lua @@ -14,7 +14,7 @@ function M.help_actions(config) local bufnr = vim.api.nvim_get_current_buf() local winnr = vim.api.nvim_get_current_win() local cursor = vim.api.nvim_win_get_cursor(winnr) - local line_diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr, cursor[1] - 1) + local line_diagnostics = vim.diagnostic.get(bufnr, { lnum = cursor[1] - 1 }) if #line_diagnostics == 0 then return nil diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 7d42df7b..29ea6bf3 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -132,7 +132,7 @@ function M.diagnostics(source) end local cursor = vim.api.nvim_win_get_cursor(winnr) - local line_diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr, cursor[1] - 1) + local line_diagnostics = vim.diagnostic.get(bufnr, { lnum = cursor[1] - 1 }) if #line_diagnostics == 0 then return nil From 2d1e09cfe6fad0fbb76b8f138afbe411d8f92312 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 25 Oct 2024 15:45:23 +0000 Subject: [PATCH 057/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 8a191f2e..80180c39 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 September 25 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 October 25 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From dab181429df7c71a75803a6a66888760f6dbf096 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:45:20 +0000 Subject: [PATCH 058/338] docs: update README.md [skip ci] --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8f355b91..b9bd7c3e 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,7 @@ [![Discord](https://img.shields.io/discord/1200633211236122665.svg)](https://discord.gg/vy6hJsTWaZ) - -[![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-) - +[![All Contributors](https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat-square)](#contributors-) > [!NOTE] @@ -608,6 +606,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Sjon-Paul Brown
Sjon-Paul Brown

💻 Renzo Mondragón
Renzo Mondragón

💻 📖 fjchen7
fjchen7

💻 + Radosław Woźniak
Radosław Woźniak

💻 From 4dc270b69365a3b0bd91ddefecae12290da6fa6b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:45:21 +0000 Subject: [PATCH 059/338] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 159 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 124 insertions(+), 35 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3cd28ec7..7d750141 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,5 +1,7 @@ { - "files": ["README.md"], + "files": [ + "README.md" + ], "imageSize": 100, "commit": false, "commitType": "docs", @@ -10,238 +12,325 @@ "name": "gptlang", "avatar_url": "https://avatars.githubusercontent.com/u/121417512?v=4", "profile": "https://github.com/gptlang", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "jellydn", "name": "Dung Duc Huynh (Kaka)", "avatar_url": "https://avatars.githubusercontent.com/u/870029?v=4", "profile": "https://productsway.com/", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "qoobes", "name": "Ahmed Haracic", "avatar_url": "https://avatars.githubusercontent.com/u/58834655?v=4", "profile": "https://qoobes.dev", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "ziontee113", "name": "Trí Thiện Nguyễn", "avatar_url": "https://avatars.githubusercontent.com/u/102876811?v=4", "profile": "https://youtube.com/@ziontee113", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "Cassius0924", "name": "He Zhizhou", "avatar_url": "https://avatars.githubusercontent.com/u/62874592?v=4", "profile": "https://github.com/Cassius0924", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "rguruprakash", "name": "Guruprakash Rajakkannu", "avatar_url": "https://avatars.githubusercontent.com/u/9963717?v=4", "profile": "https://www.linkedin.com/in/guruprakashrajakkannu/", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "kristofka", "name": "kristofka", "avatar_url": "https://avatars.githubusercontent.com/u/140354?v=4", "profile": "https://github.com/kristofka", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "PostCyberPunk", "name": "PostCyberPunk", "avatar_url": "https://avatars.githubusercontent.com/u/134976996?v=4", "profile": "https://github.com/PostCyberPunk", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "ktns", "name": "Katsuhiko Nishimra", "avatar_url": "https://avatars.githubusercontent.com/u/1302759?v=4", "profile": "https://github.com/ktns", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "errnoh", "name": "Erno Hopearuoho", "avatar_url": "https://avatars.githubusercontent.com/u/373946?v=4", "profile": "https://github.com/errnoh", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "shaungarwood", "name": "Shaun Garwood", "avatar_url": "https://avatars.githubusercontent.com/u/4156525?v=4", "profile": "https://github.com/shaungarwood", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "neutrinoA4", "name": "neutrinoA4", "avatar_url": "https://avatars.githubusercontent.com/u/122616073?v=4", "profile": "https://github.com/neutrinoA4", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "banjocat", "name": "Jack Muratore", "avatar_url": "https://avatars.githubusercontent.com/u/3247309?v=4", "profile": "https://github.com/banjocat", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "AdrielVelazquez", "name": "Adriel Velazquez", "avatar_url": "https://avatars.githubusercontent.com/u/3443378?v=4", "profile": "https://github.com/AdrielVelazquez", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "deathbeam", "name": "Tomas Slusny", "avatar_url": "https://avatars.githubusercontent.com/u/5115805?v=4", "profile": "https://github.com/deathbeam", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "nisalVD", "name": "Nisal", "avatar_url": "https://avatars.githubusercontent.com/u/30633436?v=4", "profile": "http://nisalvd.netlify.com/", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "gaardhus", "name": "Tobias Gårdhus", "avatar_url": "https://avatars.githubusercontent.com/u/46934916?v=4", "profile": "http://www.gaardhus.dk", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "PetrDlouhy", "name": "Petr Dlouhý", "avatar_url": "https://avatars.githubusercontent.com/u/156755?v=4", "profile": "https://www.patreon.com/PetrDlouhy", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "dmadisetti", "name": "Dylan Madisetti", "avatar_url": "https://avatars.githubusercontent.com/u/2689338?v=4", "profile": "http://www.dylanmadisetti.com", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "aweis89", "name": "Aaron Weisberg", "avatar_url": "https://avatars.githubusercontent.com/u/5186956?v=4", "profile": "https://github.com/aweis89", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "tlacuilose", "name": "Jose Tlacuilo", "avatar_url": "https://avatars.githubusercontent.com/u/65783495?v=4", "profile": "https://github.com/tlacuilose", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "kevintraver", "name": "Kevin Traver", "avatar_url": "https://avatars.githubusercontent.com/u/196406?v=4", "profile": "http://kevintraver.com", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "D7ry", "name": "dTry", "avatar_url": "https://avatars.githubusercontent.com/u/92609548?v=4", "profile": "https://github.com/D7ry", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "ornew", "name": "Arata Furukawa", "avatar_url": "https://avatars.githubusercontent.com/u/19766770?v=4", "profile": "https://blog.ornew.io", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "lingjie00", "name": "Ling", "avatar_url": "https://avatars.githubusercontent.com/u/64540764?v=4", "profile": "https://github.com/lingjie00", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "frolvanya", "name": "Ivan Frolov", "avatar_url": "https://avatars.githubusercontent.com/u/59515280?v=4", "profile": "https://github.com/frolvanya", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "folke", "name": "Folke Lemaitre", "avatar_url": "https://avatars.githubusercontent.com/u/292349?v=4", "profile": "http://www.folkelemaitre.com", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "GitMurf", "name": "GitMurf", "avatar_url": "https://avatars.githubusercontent.com/u/64155612?v=4", "profile": "https://github.com/GitMurf", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "festeh", "name": "Dmitrii Lipin", "avatar_url": "https://avatars.githubusercontent.com/u/6877858?v=4", "profile": "http://dimalip.in", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "jinzhongjia", "name": "jinzhongjia", "avatar_url": "https://avatars.githubusercontent.com/u/41784264?v=4", "profile": "https://nvimer.org", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "guill", "name": "guill", "avatar_url": "https://avatars.githubusercontent.com/u/3157454?v=4", "profile": "https://github.com/guill", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "sjonpaulbrown-cc", "name": "Sjon-Paul Brown", "avatar_url": "https://avatars.githubusercontent.com/u/81941908?v=4", "profile": "https://github.com/sjonpaulbrown-cc", - "contributions": ["code"] + "contributions": [ + "code" + ] }, { "login": "renxzen", "name": "Renzo Mondragón", "avatar_url": "https://avatars.githubusercontent.com/u/13023797?v=4", "profile": "https://github.com/renxzen", - "contributions": ["code", "doc"] + "contributions": [ + "code", + "doc" + ] }, { "login": "fjchen7", "name": "fjchen7", "avatar_url": "https://avatars.githubusercontent.com/u/10106636?v=4", "profile": "https://github.com/fjchen7", - "contributions": ["code"] + "contributions": [ + "code" + ] + }, + { + "login": "radwo", + "name": "Radosław Woźniak", + "avatar_url": "https://avatars.githubusercontent.com/u/184065?v=4", + "profile": "https://github.com/radwo", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, From c6765ebaf753f56212b91b818947580cea9cc5f2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:45:38 +0000 Subject: [PATCH 060/338] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .all-contributorsrc | 154 +++++++++++--------------------------------- README.md | 2 + 2 files changed, 38 insertions(+), 118 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7d750141..118130f1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,7 +1,5 @@ { - "files": [ - "README.md" - ], + "files": ["README.md"], "imageSize": 100, "commit": false, "commitType": "docs", @@ -12,325 +10,245 @@ "name": "gptlang", "avatar_url": "https://avatars.githubusercontent.com/u/121417512?v=4", "profile": "https://github.com/gptlang", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "jellydn", "name": "Dung Duc Huynh (Kaka)", "avatar_url": "https://avatars.githubusercontent.com/u/870029?v=4", "profile": "https://productsway.com/", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "qoobes", "name": "Ahmed Haracic", "avatar_url": "https://avatars.githubusercontent.com/u/58834655?v=4", "profile": "https://qoobes.dev", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "ziontee113", "name": "Trí Thiện Nguyễn", "avatar_url": "https://avatars.githubusercontent.com/u/102876811?v=4", "profile": "https://youtube.com/@ziontee113", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "Cassius0924", "name": "He Zhizhou", "avatar_url": "https://avatars.githubusercontent.com/u/62874592?v=4", "profile": "https://github.com/Cassius0924", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "rguruprakash", "name": "Guruprakash Rajakkannu", "avatar_url": "https://avatars.githubusercontent.com/u/9963717?v=4", "profile": "https://www.linkedin.com/in/guruprakashrajakkannu/", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "kristofka", "name": "kristofka", "avatar_url": "https://avatars.githubusercontent.com/u/140354?v=4", "profile": "https://github.com/kristofka", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "PostCyberPunk", "name": "PostCyberPunk", "avatar_url": "https://avatars.githubusercontent.com/u/134976996?v=4", "profile": "https://github.com/PostCyberPunk", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "ktns", "name": "Katsuhiko Nishimra", "avatar_url": "https://avatars.githubusercontent.com/u/1302759?v=4", "profile": "https://github.com/ktns", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "errnoh", "name": "Erno Hopearuoho", "avatar_url": "https://avatars.githubusercontent.com/u/373946?v=4", "profile": "https://github.com/errnoh", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "shaungarwood", "name": "Shaun Garwood", "avatar_url": "https://avatars.githubusercontent.com/u/4156525?v=4", "profile": "https://github.com/shaungarwood", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "neutrinoA4", "name": "neutrinoA4", "avatar_url": "https://avatars.githubusercontent.com/u/122616073?v=4", "profile": "https://github.com/neutrinoA4", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "banjocat", "name": "Jack Muratore", "avatar_url": "https://avatars.githubusercontent.com/u/3247309?v=4", "profile": "https://github.com/banjocat", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "AdrielVelazquez", "name": "Adriel Velazquez", "avatar_url": "https://avatars.githubusercontent.com/u/3443378?v=4", "profile": "https://github.com/AdrielVelazquez", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "deathbeam", "name": "Tomas Slusny", "avatar_url": "https://avatars.githubusercontent.com/u/5115805?v=4", "profile": "https://github.com/deathbeam", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "nisalVD", "name": "Nisal", "avatar_url": "https://avatars.githubusercontent.com/u/30633436?v=4", "profile": "http://nisalvd.netlify.com/", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "gaardhus", "name": "Tobias Gårdhus", "avatar_url": "https://avatars.githubusercontent.com/u/46934916?v=4", "profile": "http://www.gaardhus.dk", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "PetrDlouhy", "name": "Petr Dlouhý", "avatar_url": "https://avatars.githubusercontent.com/u/156755?v=4", "profile": "https://www.patreon.com/PetrDlouhy", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "dmadisetti", "name": "Dylan Madisetti", "avatar_url": "https://avatars.githubusercontent.com/u/2689338?v=4", "profile": "http://www.dylanmadisetti.com", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "aweis89", "name": "Aaron Weisberg", "avatar_url": "https://avatars.githubusercontent.com/u/5186956?v=4", "profile": "https://github.com/aweis89", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "tlacuilose", "name": "Jose Tlacuilo", "avatar_url": "https://avatars.githubusercontent.com/u/65783495?v=4", "profile": "https://github.com/tlacuilose", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "kevintraver", "name": "Kevin Traver", "avatar_url": "https://avatars.githubusercontent.com/u/196406?v=4", "profile": "http://kevintraver.com", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "D7ry", "name": "dTry", "avatar_url": "https://avatars.githubusercontent.com/u/92609548?v=4", "profile": "https://github.com/D7ry", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "ornew", "name": "Arata Furukawa", "avatar_url": "https://avatars.githubusercontent.com/u/19766770?v=4", "profile": "https://blog.ornew.io", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "lingjie00", "name": "Ling", "avatar_url": "https://avatars.githubusercontent.com/u/64540764?v=4", "profile": "https://github.com/lingjie00", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "frolvanya", "name": "Ivan Frolov", "avatar_url": "https://avatars.githubusercontent.com/u/59515280?v=4", "profile": "https://github.com/frolvanya", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "folke", "name": "Folke Lemaitre", "avatar_url": "https://avatars.githubusercontent.com/u/292349?v=4", "profile": "http://www.folkelemaitre.com", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "GitMurf", "name": "GitMurf", "avatar_url": "https://avatars.githubusercontent.com/u/64155612?v=4", "profile": "https://github.com/GitMurf", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "festeh", "name": "Dmitrii Lipin", "avatar_url": "https://avatars.githubusercontent.com/u/6877858?v=4", "profile": "http://dimalip.in", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "jinzhongjia", "name": "jinzhongjia", "avatar_url": "https://avatars.githubusercontent.com/u/41784264?v=4", "profile": "https://nvimer.org", - "contributions": [ - "doc" - ] + "contributions": ["doc"] }, { "login": "guill", "name": "guill", "avatar_url": "https://avatars.githubusercontent.com/u/3157454?v=4", "profile": "https://github.com/guill", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "sjonpaulbrown-cc", "name": "Sjon-Paul Brown", "avatar_url": "https://avatars.githubusercontent.com/u/81941908?v=4", "profile": "https://github.com/sjonpaulbrown-cc", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "renxzen", "name": "Renzo Mondragón", "avatar_url": "https://avatars.githubusercontent.com/u/13023797?v=4", "profile": "https://github.com/renxzen", - "contributions": [ - "code", - "doc" - ] + "contributions": ["code", "doc"] }, { "login": "fjchen7", "name": "fjchen7", "avatar_url": "https://avatars.githubusercontent.com/u/10106636?v=4", "profile": "https://github.com/fjchen7", - "contributions": [ - "code" - ] + "contributions": ["code"] }, { "login": "radwo", "name": "Radosław Woźniak", "avatar_url": "https://avatars.githubusercontent.com/u/184065?v=4", "profile": "https://github.com/radwo", - "contributions": [ - "code" - ] + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b9bd7c3e..67110d2b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![Discord](https://img.shields.io/discord/1200633211236122665.svg)](https://discord.gg/vy6hJsTWaZ) + [![All Contributors](https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat-square)](#contributors-) + > [!NOTE] From e4cb1fc27e0def7571e5329f522a79e9555a5502 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 25 Oct 2024 15:48:07 +0000 Subject: [PATCH 061/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 80180c39..f52bac89 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -591,7 +591,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -606,7 +606,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From 4caf0f60750e63fb110e26f5b9be0e2910f94b7e Mon Sep 17 00:00:00 2001 From: gptlang Date: Tue, 29 Oct 2024 20:18:21 +0000 Subject: [PATCH 062/338] disable stream for o1 models --- lua/CopilotChat/copilot.lua | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 4e6ddfce..ad176b4c 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -181,6 +181,15 @@ local function generate_embeddings_message(embeddings) return out end +--- Check if the model can stream +--- @param model_name string: The model name to check +local function can_stream(model_name) + if vim.startswith(model_name, 'o1') then + return false + end + return true +end + local function generate_ask_request( history, prompt, @@ -222,15 +231,23 @@ local function generate_ask_request( role = 'user', }) - return { - intent = true, - model = model, - n = 1, - stream = true, - temperature = temperature, - top_p = 1, - messages = messages, - } + if can_stream(model) then + return { + intent = true, + model = model, + n = 1, + stream = true, + temperature = temperature, + top_p = 1, + messages = messages, + } + else + return { + messages = messages, + stream = false, + model = model, + } + end end local function generate_embedding_request(inputs, model) From ce2a919e7b0ad4a7b4fce8ae177444e4774244c6 Mon Sep 17 00:00:00 2001 From: gptlang Date: Tue, 29 Oct 2024 20:31:34 +0000 Subject: [PATCH 063/338] disable system role for o1 models --- lua/CopilotChat/copilot.lua | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index ad176b4c..8d722c87 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -183,11 +183,11 @@ end --- Check if the model can stream --- @param model_name string: The model name to check -local function can_stream(model_name) +local function is_o1(model_name) if vim.startswith(model_name, 'o1') then - return false + return true end - return true + return false end local function generate_ask_request( @@ -201,10 +201,15 @@ local function generate_ask_request( ) local messages = {} + local system_role = 'system' + if is_o1(model) then + system_role = 'user' + end + if system_prompt ~= '' then table.insert(messages, { content = system_prompt, - role = 'system', + role = system_role, }) end @@ -215,14 +220,14 @@ local function generate_ask_request( if embeddings and #embeddings.files > 0 then table.insert(messages, { content = embeddings.header .. table.concat(embeddings.files, ''), - role = 'system', + role = system_role, }) end if selection ~= '' then table.insert(messages, { content = selection, - role = 'system', + role = system_role, }) end @@ -231,7 +236,13 @@ local function generate_ask_request( role = 'user', }) - if can_stream(model) then + if is_o1(model) then + return { + messages = messages, + stream = false, + model = model, + } + else return { intent = true, model = model, @@ -241,12 +252,6 @@ local function generate_ask_request( top_p = 1, messages = messages, } - else - return { - messages = messages, - stream = false, - model = model, - } end end From a6f2e755b6e20bd911e11c00eae2978d2ed0c23c Mon Sep 17 00:00:00 2001 From: gptlang Date: Tue, 29 Oct 2024 20:51:57 +0000 Subject: [PATCH 064/338] use callback instead of stream for o1 --- lua/CopilotChat/copilot.lua | 176 +++++++++++++++++++++++------------- 1 file changed, 114 insertions(+), 62 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 8d722c87..99184d20 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -441,6 +441,118 @@ function Copilot:ask(prompt, opts) local errored = false local full_response = '' + ---@type fun(err: string, line: string)? + local stream_func = function(err, line) + if not line or errored then + return + end + + if err or vim.startswith(line, '{"error"') then + err = 'Failed to get response: ' .. (err and vim.inspect(err) or line) + errored = true + log.error(err) + if self.current_job and on_error then + on_error(err) + end + return + end + + line = line:gsub('data: ', '') + if line == '' then + return + elseif line == '[DONE]' then + log.trace('Full response: ' .. full_response) + self.token_count = self.token_count + tiktoken.count(full_response) + + if self.current_job and on_done then + on_done(full_response, self.token_count + current_count) + end + + table.insert(self.history, { + content = full_response, + role = 'assistant', + }) + return + end + + local ok, content = pcall(vim.json.decode, line, { + luanil = { + object = true, + array = true, + }, + }) + + if not ok then + err = 'Failed parse response: \n' .. line .. '\n' .. vim.inspect(content) + log.error(err) + return + end + + if not content.choices or #content.choices == 0 then + return + end + + content = content.choices[1].delta.content + if not content then + return + end + + if self.current_job and on_progress then + on_progress(content) + end + + -- Collect full response incrementally so we can insert it to history later + full_response = full_response .. content + end + if is_o1(model) then + stream_func = nil + end + + ---@type fun(response: table)? + local nonstream_callback = function(response) + if response.status ~= 200 then + local err = 'Failed to get response: ' .. tostring(response.status) + log.error(err) + if on_error then + on_error(err) + end + return + end + + local ok, content = pcall(vim.json.decode, response.body, { + luanil = { + object = true, + array = true, + }, + }) + + if not ok then + local err = 'Failed parse response: ' .. vim.inspect(content) + log.error(err) + if on_error then + on_error(err) + end + return + end + + full_response = content.choices[1].message.content + if on_progress then + on_progress(full_response) + end + self.token_count = self.token_count + tiktoken.count(full_response) + if on_done then + on_done(full_response, self.token_count + current_count) + end + + table.insert(self.history, { + content = full_response, + role = 'assistant', + }) + end + + if not is_o1(model) then + nonstream_callback = nil + end self:with_auth(function() local headers = generate_headers(self.token.token, self.sessionid, self.machineid) @@ -451,6 +563,7 @@ function Copilot:ask(prompt, opts) body = temp_file(body), proxy = self.proxy, insecure = self.allow_insecure, + callback = nonstream_callback, on_error = function(err) err = 'Failed to get response: ' .. vim.inspect(err) log.error(err) @@ -458,68 +571,7 @@ function Copilot:ask(prompt, opts) on_error(err) end end, - stream = function(err, line) - if not line or errored then - return - end - - if err or vim.startswith(line, '{"error"') then - err = 'Failed to get response: ' .. (err and vim.inspect(err) or line) - errored = true - log.error(err) - if self.current_job and on_error then - on_error(err) - end - return - end - - line = line:gsub('data: ', '') - if line == '' then - return - elseif line == '[DONE]' then - log.trace('Full response: ' .. full_response) - self.token_count = self.token_count + tiktoken.count(full_response) - - if self.current_job and on_done then - on_done(full_response, self.token_count + current_count) - end - - table.insert(self.history, { - content = full_response, - role = 'assistant', - }) - return - end - - local ok, content = pcall(vim.json.decode, line, { - luanil = { - object = true, - array = true, - }, - }) - - if not ok then - err = 'Failed parse response: \n' .. line .. '\n' .. vim.inspect(content) - log.error(err) - return - end - - if not content.choices or #content.choices == 0 then - return - end - - content = content.choices[1].delta.content - if not content then - return - end - - if self.current_job and on_progress then - on_progress(content) - end - - -- Collect full response incrementally so we can insert it to history later - full_response = full_response .. content - end, + stream = stream_func, }) :after(function() self.current_job = nil From 93a9f29b88f23c110316213813a388b96235becc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Oct 2024 21:00:57 +0000 Subject: [PATCH 065/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index f52bac89..ad023f5a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 October 25 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 October 29 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 82604838fa511d1cbf8174e99f2fbc3a0dc0abe4 Mon Sep 17 00:00:00 2001 From: gptlang Date: Tue, 29 Oct 2024 21:19:49 +0000 Subject: [PATCH 066/338] ensure claude enabled --- lua/CopilotChat/copilot.lua | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 99184d20..3d08f239 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -54,6 +54,7 @@ local version_headers = { ['editor-plugin-version'] = 'CopilotChat.nvim/2.0.0', ['user-agent'] = 'CopilotChat.nvim/2.0.0', } +local claude_enabled = false local function uuid() local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' @@ -364,6 +365,36 @@ function Copilot:with_auth(on_done, on_error) end end +function Copilot:enable_claude() + if claude_enabled then + return + end + self:with_auth(function() + local url = 'https://api.githubcopilot.com/models/claude-3.5-sonnet/policy' + local headers = generate_headers(self.token.token, self.sessionid, self.machineid) + curl.post(url, { + timeout = timeout, + headers = headers, + proxy = self.proxy, + insecure = self.allow_insecure, + on_error = function(err) + err = 'Failed to enable Claude: ' .. vim.inspect(err) + log.error(err) + end, + body = temp_file('{"state": "enabled"}'), + callback = function(response) + if response.status ~= 200 then + local msg = 'Failed to enable Claude: ' .. tostring(response.status) + log.error(msg) + return + end + claude_enabled = true + log.info('Claude enabled') + end, + }) + end) +end + --- Ask a question to Copilot ---@param prompt string: The prompt to send to Copilot ---@param opts CopilotChat.copilot.ask.opts: Options for the request @@ -420,6 +451,10 @@ function Copilot:ask(prompt, opts) embeddings_message.files = filtered_files end + if vim.startswith(model, 'claude') then + self:enable_claude() + end + local url = 'https://api.githubcopilot.com/chat/completions' local body = vim.json.encode( generate_ask_request( From 62a02a202b8051ea50d312972d719b5fea8741d1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Oct 2024 09:26:55 +0100 Subject: [PATCH 067/338] Only ask questions after claude request completes This prevents issue where claude request takes longer than auth and breaks on first ask. Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 68 +++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 3d08f239..b3107eba 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -184,11 +184,8 @@ end --- Check if the model can stream --- @param model_name string: The model name to check -local function is_o1(model_name) - if vim.startswith(model_name, 'o1') then - return true - end - return false +local function can_stream(model_name) + return not vim.startswith(model_name, 'o1') end local function generate_ask_request( @@ -203,7 +200,7 @@ local function generate_ask_request( local messages = {} local system_role = 'system' - if is_o1(model) then + if not can_stream(model) then system_role = 'user' end @@ -237,13 +234,7 @@ local function generate_ask_request( role = 'user', }) - if is_o1(model) then - return { - messages = messages, - stream = false, - model = model, - } - else + if can_stream(model) then return { intent = true, model = model, @@ -253,6 +244,12 @@ local function generate_ask_request( top_p = 1, messages = messages, } + else + return { + messages = messages, + stream = false, + model = model, + } end end @@ -365,11 +362,13 @@ function Copilot:with_auth(on_done, on_error) end end -function Copilot:enable_claude() - if claude_enabled then - return - end +function Copilot:with_claude(on_done, on_error) self:with_auth(function() + if claude_enabled then + on_done() + return + end + local url = 'https://api.githubcopilot.com/models/claude-3.5-sonnet/policy' local headers = generate_headers(self.token.token, self.sessionid, self.machineid) curl.post(url, { @@ -380,16 +379,24 @@ function Copilot:enable_claude() on_error = function(err) err = 'Failed to enable Claude: ' .. vim.inspect(err) log.error(err) + if on_error then + on_error(err) + end end, body = temp_file('{"state": "enabled"}'), callback = function(response) if response.status ~= 200 then local msg = 'Failed to enable Claude: ' .. tostring(response.status) log.error(msg) + if on_error then + on_error(msg) + end return end + claude_enabled = true log.info('Claude enabled') + on_done() end, }) end) @@ -438,6 +445,7 @@ function Copilot:ask(prompt, opts) current_count = current_count + tiktoken.count(system_prompt) current_count = current_count + tiktoken.count(selection_message) + -- Limit the number of files to send if #embeddings_message.files > 0 then local filtered_files = {} current_count = current_count + tiktoken.count(embeddings_message.header) @@ -451,10 +459,6 @@ function Copilot:ask(prompt, opts) embeddings_message.files = filtered_files end - if vim.startswith(model, 'claude') then - self:enable_claude() - end - local url = 'https://api.githubcopilot.com/chat/completions' local body = vim.json.encode( generate_ask_request( @@ -476,8 +480,8 @@ function Copilot:ask(prompt, opts) local errored = false local full_response = '' - ---@type fun(err: string, line: string)? - local stream_func = function(err, line) + + local function stream_func(err, line) if not line or errored then return end @@ -539,12 +543,8 @@ function Copilot:ask(prompt, opts) -- Collect full response incrementally so we can insert it to history later full_response = full_response .. content end - if is_o1(model) then - stream_func = nil - end - ---@type fun(response: table)? - local nonstream_callback = function(response) + local function callback_func(response) if response.status ~= 200 then local err = 'Failed to get response: ' .. tostring(response.status) log.error(err) @@ -585,11 +585,13 @@ function Copilot:ask(prompt, opts) }) end - if not is_o1(model) then - nonstream_callback = nil + local is_stream = can_stream(model) + local with_auth = self.with_auth + if vim.startswith(model, 'claude') then + with_auth = self.with_claude end - self:with_auth(function() + with_auth(self, function() local headers = generate_headers(self.token.token, self.sessionid, self.machineid) self.current_job = curl .post(url, { @@ -598,7 +600,8 @@ function Copilot:ask(prompt, opts) body = temp_file(body), proxy = self.proxy, insecure = self.allow_insecure, - callback = nonstream_callback, + callback = (not is_stream) and callback_func or nil, + stream = is_stream and stream_func or nil, on_error = function(err) err = 'Failed to get response: ' .. vim.inspect(err) log.error(err) @@ -606,7 +609,6 @@ function Copilot:ask(prompt, opts) on_error(err) end end, - stream = stream_func, }) :after(function() self.current_job = nil From cedea3bf9eedc11ceb4acfd0e9578ec08cf6fba8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Oct 2024 08:33:49 +0000 Subject: [PATCH 068/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ad023f5a..42cdbf50 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 October 29 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 October 30 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From de6deed9b9f82855ca7a31029782ca61125f6010 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Oct 2024 09:40:27 +0100 Subject: [PATCH 069/338] Check for manual Claude enable in response Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index b3107eba..8a48ad22 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -369,6 +369,10 @@ function Copilot:with_claude(on_done, on_error) return end + local business_check = 'cannot enable policy inline for business users' + local business_msg = + 'Claude is probably enabled (for business users needs to be enabled manually).' + local url = 'https://api.githubcopilot.com/models/claude-3.5-sonnet/policy' local headers = generate_headers(self.token.token, self.sessionid, self.machineid) curl.post(url, { @@ -378,6 +382,13 @@ function Copilot:with_claude(on_done, on_error) insecure = self.allow_insecure, on_error = function(err) err = 'Failed to enable Claude: ' .. vim.inspect(err) + if string.find(err, business_check) then + claude_enabled = true + log.info(business_msg) + on_done() + return + end + log.error(err) if on_error then on_error(err) @@ -386,7 +397,15 @@ function Copilot:with_claude(on_done, on_error) body = temp_file('{"state": "enabled"}'), callback = function(response) if response.status ~= 200 then + if string.find(response.content, business_check) then + claude_enabled = true + log.info(business_msg) + on_done() + return + end + local msg = 'Failed to enable Claude: ' .. tostring(response.status) + log.error(msg) if on_error then on_error(msg) From 4fe421b586ab41b70a78f5ac85478bbf01a495a0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Oct 2024 10:29:51 +0100 Subject: [PATCH 070/338] Use response.body instead of response.content for reading response Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 8a48ad22..0d516595 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -397,7 +397,7 @@ function Copilot:with_claude(on_done, on_error) body = temp_file('{"state": "enabled"}'), callback = function(response) if response.status ~= 200 then - if string.find(response.content, business_check) then + if string.find(tostring(response.body), business_check) then claude_enabled = true log.info(business_msg) on_done() From eafa9187901eee180e4c28eb6f1fd26a8d1b7c69 Mon Sep 17 00:00:00 2001 From: JakubPecenka Date: Wed, 30 Oct 2024 09:36:55 +0100 Subject: [PATCH 071/338] fix: tiktoken download fail if cache folder doesn't exist --- lua/CopilotChat/tiktoken.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index e19a39da..9ef4be0a 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -5,6 +5,7 @@ local tiktoken_core = nil ---@param fname string ---@return string local function get_cache_path(fname) + vim.fn.mkdir(vim.fn.stdpath('cache'), 'p') return vim.fn.stdpath('cache') .. '/' .. fname end From 07a6dd665a9f446e08af08e7146b757bcbe47249 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:35:13 +0100 Subject: [PATCH 072/338] docs: add JakubPecenka as a contributor for code (#441) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 118130f1..5ce8012f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -249,6 +249,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/184065?v=4", "profile": "https://github.com/radwo", "contributions": ["code"] + }, + { + "login": "JakubPecenka", + "name": "JakubPecenka", + "avatar_url": "https://avatars.githubusercontent.com/u/87969308?v=4", + "profile": "https://github.com/JakubPecenka", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 67110d2b..625adfa0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square)](#contributors-) @@ -610,6 +610,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d fjchen7
fjchen7

💻 Radosław Woźniak
Radosław Woźniak

💻 + + JakubPecenka
JakubPecenka

💻 + From 59068aef9812a9cab4c906684933073defe87048 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Oct 2024 11:12:45 +0100 Subject: [PATCH 073/338] Log response body as well on parse failure Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 0d516595..bc0682dd 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -541,7 +541,7 @@ function Copilot:ask(prompt, opts) }) if not ok then - err = 'Failed parse response: \n' .. line .. '\n' .. vim.inspect(content) + err = 'Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line log.error(err) return end @@ -581,7 +581,7 @@ function Copilot:ask(prompt, opts) }) if not ok then - local err = 'Failed parse response: ' .. vim.inspect(content) + local err = 'Failed to parse response: ' .. vim.inspect(content) .. '\n' .. response.body log.error(err) if on_error then on_error(err) @@ -748,7 +748,7 @@ function Copilot:embed(inputs, opts) if not ok then local err = vim.inspect(content) - log.error('Failed parse response: ' .. err) + log.error('Failed to parse response: ' .. err .. '\n' .. response.body) resolve() return end From 6f0a5e1459b7d079a75c2eb90bc79e3a361d4955 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Oct 2024 10:14:15 +0000 Subject: [PATCH 074/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 42cdbf50..4eb6d115 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -591,7 +591,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -606,7 +606,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From e172d7394e20757b688a1a93eab79e75d20562ab Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Oct 2024 11:24:15 +0100 Subject: [PATCH 075/338] fix: Ensure that start_col and end_col selection is awlays within bounds Closes #432 Signed-off-by: Tomas Slusny --- lua/CopilotChat/select.lua | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 29ea6bf3..cb958721 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -1,27 +1,46 @@ local M = {} local function get_selection_lines(bufnr, start_line, start_col, finish_line, finish_col, full_line) + -- Exit if no actual selection if start_line == finish_line and start_col == finish_col then return nil end + -- Get line lengths before swapping + local function get_line_length(line) + return #vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)[1] + end + + -- Swap positions if selection is backwards if start_line > finish_line or (start_line == finish_line and start_col > finish_col) then start_line, finish_line = finish_line, start_line start_col, finish_col = finish_col, start_col end + -- Handle full line selection if full_line then start_col = 1 + finish_col = get_line_length(finish_line) + end + + -- Ensure columns are within valid bounds + start_col = math.max(1, math.min(start_col, get_line_length(start_line))) + finish_col = math.max(start_col, math.min(finish_col, get_line_length(finish_line))) + + -- Get selected text + local ok, lines = pcall( + vim.api.nvim_buf_get_text, + bufnr, + start_line - 1, + start_col - 1, + finish_line - 1, + finish_col, + {} + ) + if not ok then + return nil end - local finish_line_len = #vim.api.nvim_buf_get_lines(bufnr, finish_line - 1, finish_line, false)[1] - if finish_col > finish_line_len or full_line then - finish_col = finish_line_len - end - - local lines = - vim.api.nvim_buf_get_text(bufnr, start_line - 1, start_col - 1, finish_line - 1, finish_col, {}) - local lines_content = table.concat(lines, '\n') if vim.trim(lines_content) == '' then return nil From 44fc8c4857c1fcf073b397eceefe5cf30cf4fd15 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Oct 2024 19:10:36 +0100 Subject: [PATCH 076/338] Move cache path and IO outside of vim.loop Signed-off-by: Tomas Slusny --- lua/CopilotChat/tiktoken.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 9ef4be0a..94c76be1 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -26,10 +26,11 @@ local function load_tiktoken_data(done, model) if model ~= nil and vim.startswith(model, 'gpt-4o') then tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/o200k_base.tiktoken' end + -- Take filename after the last slash of the url + local cache_path = get_cache_path(tiktoken_url:match('.+/(.+)')) + local async async = vim.loop.new_async(function() - -- Take filename after the last slash of the url - local cache_path = get_cache_path(tiktoken_url:match('.+/(.+)')) if not file_exists(cache_path) then vim.schedule(function() curl.get(tiktoken_url, { From 3322c1a7264b17cc73aad0c627dd909d3e01eb1f Mon Sep 17 00:00:00 2001 From: thomastthai <16532581+thomastthai@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:12:53 -0700 Subject: [PATCH 077/338] Added Post-Installation to README.md (#446) * Update README.md Recommend verifying "Copilot chat in the IDE" is enabled in post-installation. * Update README.md Changed Post-Installation heading to H3. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 625adfa0..b5902e05 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,10 @@ require("CopilotChat").setup { See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/master/nvim/.config/nvim/lua/config/copilot.lua#L14) +### Post-Installation + +Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabled. + ## Usage ### Commands From 6249dbbb444d9aae7369fb7e49420d37d7ddb56d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Nov 2024 20:13:14 +0000 Subject: [PATCH 078/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 4eb6d115..e4fa3d00 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 October 30 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 01 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -119,6 +119,12 @@ See @deathbeam for configuration +POST-INSTALLATION ~ + +Verify "Copilot chat in the IDE " is +enabled. + + USAGE *CopilotChat-usage* From ae6b0c08e7f12c670b742886e6985620b566a03c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:13:38 +0100 Subject: [PATCH 079/338] docs: add thomastthai as a contributor for doc (#448) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 5ce8012f..3f75b37c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -256,6 +256,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/87969308?v=4", "profile": "https://github.com/JakubPecenka", "contributions": ["code"] + }, + { + "login": "thomastthai", + "name": "thomastthai", + "avatar_url": "https://avatars.githubusercontent.com/u/16532581?v=4", + "profile": "https://github.com/thomastthai", + "contributions": ["doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b5902e05..9a9e5b95 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square)](#contributors-) @@ -616,6 +616,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d JakubPecenka
JakubPecenka

💻 + thomastthai
thomastthai

📖 From f492023e9e42de3ec3d5703592aca94ce6175ab5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Nov 2024 20:36:33 +0100 Subject: [PATCH 080/338] Grab model data dynamically instead of hardcoded - Dynamically resolve max token count - Dynamically resolve tokenizers - Use only stream function not both callback and stream and distinguish type of response instead Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 500 +++++++++++++++++------------------ lua/CopilotChat/init.lua | 7 +- lua/CopilotChat/tiktoken.lua | 56 ++-- 3 files changed, 276 insertions(+), 287 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index bc0682dd..6ed40a54 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -32,17 +32,16 @@ ---@field save fun(self: CopilotChat.Copilot, name: string, path: string):nil ---@field load fun(self: CopilotChat.Copilot, name: string, path: string):table ---@field running fun(self: CopilotChat.Copilot):boolean ----@field select_model fun(self: CopilotChat.Copilot, callback: fun(table):nil):nil +---@field list_models fun(self: CopilotChat.Copilot, callback: fun(table):nil):nil local log = require('plenary.log') local curl = require('plenary.curl') +local prompts = require('CopilotChat.prompts') +local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') local class = utils.class local join = utils.join local temp_file = utils.temp_file -local prompts = require('CopilotChat.prompts') -local tiktoken = require('CopilotChat.tiktoken') -local max_tokens = 8192 local timeout = 30000 local version_headers = { ['editor-version'] = 'Neovim/' @@ -54,7 +53,6 @@ local version_headers = { ['editor-plugin-version'] = 'CopilotChat.nvim/2.0.0', ['user-agent'] = 'CopilotChat.nvim/2.0.0', } -local claude_enabled = false local function uuid() local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' @@ -302,7 +300,8 @@ local Copilot = class(function(self, proxy, allow_insecure) self.sessionid = nil self.machineid = machine_id() self.current_job = nil - self.models_cache = nil + self.models = nil + self.clause_enabled = false end) function Copilot:with_auth(on_done, on_error) @@ -362,63 +361,107 @@ function Copilot:with_auth(on_done, on_error) end end -function Copilot:with_claude(on_done, on_error) - self:with_auth(function() - if claude_enabled then +function Copilot:with_models(on_done, on_error) + if self.models ~= nil then + on_done() + return + end + + local url = 'https://api.githubcopilot.com/models' + local headers = generate_headers(self.token.token, self.sessionid, self.machineid) + curl.get(url, { + timeout = timeout, + headers = headers, + proxy = self.proxy, + insecure = self.allow_insecure, + on_error = function(err) + err = 'Failed to get response: ' .. vim.inspect(err) + log.error(err) + if on_error then + on_error(err) + end + end, + callback = function(response) + if response.status ~= 200 then + local msg = 'Failed to fetch models: ' .. tostring(response.status) + log.error(msg) + if on_error then + on_error(msg) + end + return + end + + -- Find chat models + local models = vim.json.decode(response.body)['data'] + local out = {} + for _, model in ipairs(models) do + if model['capabilities']['type'] == 'chat' then + out[model['id']] = model + end + end + + log.info('Models fetched') + self.models = out on_done() - return - end + end, + }) +end - local business_check = 'cannot enable policy inline for business users' - local business_msg = - 'Claude is probably enabled (for business users needs to be enabled manually).' +function Copilot:with_claude(model, on_done, on_error) + if self.claude_enabled or not vim.startswith(model, 'claude') then + on_done() + return + end - local url = 'https://api.githubcopilot.com/models/claude-3.5-sonnet/policy' - local headers = generate_headers(self.token.token, self.sessionid, self.machineid) - curl.post(url, { - timeout = timeout, - headers = headers, - proxy = self.proxy, - insecure = self.allow_insecure, - on_error = function(err) - err = 'Failed to enable Claude: ' .. vim.inspect(err) - if string.find(err, business_check) then - claude_enabled = true + local business_check = 'cannot enable policy inline for business users' + local business_msg = + 'Claude is probably enabled (for business users needs to be enabled manually).' + + local url = 'https://api.githubcopilot.com/models/claude-3.5-sonnet/policy' + local headers = generate_headers(self.token.token, self.sessionid, self.machineid) + curl.post(url, { + timeout = timeout, + headers = headers, + proxy = self.proxy, + insecure = self.allow_insecure, + on_error = function(err) + err = 'Failed to enable Claude: ' .. vim.inspect(err) + if string.find(err, business_check) then + self.claude_enabled = true + log.info(business_msg) + on_done() + return + end + + log.error(err) + if on_error then + on_error(err) + end + end, + body = temp_file('{"state": "enabled"}'), + callback = function(response) + if response.status ~= 200 then + if string.find(tostring(response.body), business_check) then + self.claude_enabled = true log.info(business_msg) on_done() return end - log.error(err) - if on_error then - on_error(err) - end - end, - body = temp_file('{"state": "enabled"}'), - callback = function(response) - if response.status ~= 200 then - if string.find(tostring(response.body), business_check) then - claude_enabled = true - log.info(business_msg) - on_done() - return - end - - local msg = 'Failed to enable Claude: ' .. tostring(response.status) + local msg = 'Failed to enable Claude: ' .. tostring(response.status) - log.error(msg) - if on_error then - on_error(msg) - end - return + log.error(msg) + if on_error then + on_error(msg) end + return + end - claude_enabled = true - log.info('Claude enabled') - on_done() - end, - }) - end) + self.claude_enabled = true + log.info('Claude enabled') + on_done() + end, + }) end --- Ask a question to Copilot @@ -453,235 +496,176 @@ function Copilot:ask(prompt, opts) self:stop() end - local selection_message = - generate_selection_message(filename, filetype, start_row, end_row, selection) - local embeddings_message = generate_embeddings_message(embeddings) - - -- Count tokens - self.token_count = self.token_count + tiktoken.count(prompt) - - local current_count = 0 - current_count = current_count + tiktoken.count(system_prompt) - current_count = current_count + tiktoken.count(selection_message) - - -- Limit the number of files to send - if #embeddings_message.files > 0 then - local filtered_files = {} - current_count = current_count + tiktoken.count(embeddings_message.header) - for _, file in ipairs(embeddings_message.files) do - local file_count = current_count + tiktoken.count(file) - if file_count + self.token_count < max_tokens then - current_count = file_count - table.insert(filtered_files, file) - end - end - embeddings_message.files = filtered_files - end - - local url = 'https://api.githubcopilot.com/chat/completions' - local body = vim.json.encode( - generate_ask_request( - self.history, - prompt, - embeddings_message, - selection_message, - system_prompt, - model, - temperature - ) - ) - - -- Add the prompt to history after we have encoded the request - table.insert(self.history, { - content = prompt, - role = 'user', - }) - - local errored = false - local full_response = '' - - local function stream_func(err, line) - if not line or errored then - return - end + self:with_auth(function() + self:with_models(function() + local capabilities = self.models[model] and self.models[model].capabilities + or { limits = { max_prompt_tokens = 8192 }, tokenizer = 'cl100k_base' } + local max_tokens = capabilities.limits.max_prompt_tokens -- FIXME: Is max_prompt_tokens the right limit? + local tokenizer = capabilities.tokenizer + log.debug('Max tokens: ' .. max_tokens) + log.debug('Tokenizer: ' .. tokenizer) + + local selection_message = + generate_selection_message(filename, filetype, start_row, end_row, selection) + local embeddings_message = generate_embeddings_message(embeddings) + + tiktoken.load(tokenizer, function() + -- Count tokens + self.token_count = self.token_count + tiktoken.count(prompt) + local current_count = 0 + current_count = current_count + tiktoken.count(system_prompt) + current_count = current_count + tiktoken.count(selection_message) + + -- Limit the number of files to send + if #embeddings_message.files > 0 then + local filtered_files = {} + current_count = current_count + tiktoken.count(embeddings_message.header) + for _, file in ipairs(embeddings_message.files) do + local file_count = current_count + tiktoken.count(file) + if file_count + self.token_count < max_tokens then + current_count = file_count + table.insert(filtered_files, file) + end + end + embeddings_message.files = filtered_files + end - if err or vim.startswith(line, '{"error"') then - err = 'Failed to get response: ' .. (err and vim.inspect(err) or line) - errored = true - log.error(err) - if self.current_job and on_error then - on_error(err) - end - return - end + local url = 'https://api.githubcopilot.com/chat/completions' + local body = vim.json.encode( + generate_ask_request( + self.history, + prompt, + embeddings_message, + selection_message, + system_prompt, + model, + temperature + ) + ) - line = line:gsub('data: ', '') - if line == '' then - return - elseif line == '[DONE]' then - log.trace('Full response: ' .. full_response) - self.token_count = self.token_count + tiktoken.count(full_response) + -- Add the prompt to history after we have encoded the request + table.insert(self.history, { + content = prompt, + role = 'user', + }) - if self.current_job and on_done then - on_done(full_response, self.token_count + current_count) - end + local errored = false + local full_response = '' - table.insert(self.history, { - content = full_response, - role = 'assistant', - }) - return - end + local function stream_func(err, line) + if not line or errored then + return + end - local ok, content = pcall(vim.json.decode, line, { - luanil = { - object = true, - array = true, - }, - }) + if err or vim.startswith(line, '{"error"') then + err = 'Failed to get response: ' .. (err and vim.inspect(err) or line) + errored = true + log.error(err) + if self.current_job and on_error then + on_error(err) + end + return + end - if not ok then - err = 'Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line - log.error(err) - return - end + line = line:gsub('data: ', '') + if line == '' then + return + elseif line == '[DONE]' then + log.trace('Full response: ' .. full_response) + self.token_count = self.token_count + tiktoken.count(full_response) + + if self.current_job and on_done then + on_done(full_response, self.token_count + current_count) + end + + table.insert(self.history, { + content = full_response, + role = 'assistant', + }) + return + end - if not content.choices or #content.choices == 0 then - return - end + local ok, content = pcall(vim.json.decode, line, { + luanil = { + object = true, + array = true, + }, + }) - content = content.choices[1].delta.content - if not content then - return - end + if not ok then + err = 'Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line + log.error(err) + return + end - if self.current_job and on_progress then - on_progress(content) - end + if not content.choices or #content.choices == 0 then + return + end - -- Collect full response incrementally so we can insert it to history later - full_response = full_response .. content - end + local choice = content.choices[1] + local is_full = choice.message ~= nil + content = is_full and choice.message.content or choice.delta.content - local function callback_func(response) - if response.status ~= 200 then - local err = 'Failed to get response: ' .. tostring(response.status) - log.error(err) - if on_error then - on_error(err) - end - return - end + if not content then + return + end - local ok, content = pcall(vim.json.decode, response.body, { - luanil = { - object = true, - array = true, - }, - }) + if self.current_job and on_progress then + on_progress(content) + end - if not ok then - local err = 'Failed to parse response: ' .. vim.inspect(content) .. '\n' .. response.body - log.error(err) - if on_error then - on_error(err) - end - return - end + if is_full then + log.trace('Full response: ' .. content) + self.token_count = self.token_count + tiktoken.count(content) - full_response = content.choices[1].message.content - if on_progress then - on_progress(full_response) - end - self.token_count = self.token_count + tiktoken.count(full_response) - if on_done then - on_done(full_response, self.token_count + current_count) - end + if self.current_job and on_done then + on_done(content, self.token_count + current_count) + end - table.insert(self.history, { - content = full_response, - role = 'assistant', - }) - end + table.insert(self.history, { + content = full_response, + role = 'assistant', + }) + return + end - local is_stream = can_stream(model) - local with_auth = self.with_auth - if vim.startswith(model, 'claude') then - with_auth = self.with_claude - end + -- Collect full response incrementally so we can insert it to history later + full_response = full_response .. content + end - with_auth(self, function() - local headers = generate_headers(self.token.token, self.sessionid, self.machineid) - self.current_job = curl - .post(url, { - timeout = timeout, - headers = headers, - body = temp_file(body), - proxy = self.proxy, - insecure = self.allow_insecure, - callback = (not is_stream) and callback_func or nil, - stream = is_stream and stream_func or nil, - on_error = function(err) - err = 'Failed to get response: ' .. vim.inspect(err) - log.error(err) - if self.current_job and on_error then - on_error(err) - end - end, - }) - :after(function() - self.current_job = nil + self:with_claude(model, function() + self.current_job = curl + .post(url, { + timeout = timeout, + headers = generate_headers(self.token.token, self.sessionid, self.machineid), + body = temp_file(body), + proxy = self.proxy, + insecure = self.allow_insecure, + stream = stream_func, + on_error = function(err) + err = 'Failed to get response: ' .. vim.inspect(err) + log.error(err) + if self.current_job and on_error then + on_error(err) + end + end, + }) + :after(function() + self.current_job = nil + end) + end, on_error) end) + end, on_error) end, on_error) end ---- Fetch & allow model selection +--- List available models ---@param callback fun(table):nil -function Copilot:select_model(callback) - if self.models_cache ~= nil then - callback(self.models_cache) - return - end - - local url = 'https://api.githubcopilot.com/models' +function Copilot:list_models(callback) self:with_auth(function() - local headers = generate_headers(self.token.token, self.sessionid, self.machineid) - curl.get(url, { - timeout = timeout, - headers = headers, - proxy = self.proxy, - insecure = self.allow_insecure, - on_error = function(err) - err = 'Failed to get response: ' .. vim.inspect(err) - log.error(err) - end, - callback = function(response) - if response.status ~= 200 then - local msg = 'Failed to fetch models: ' .. tostring(response.status) - log.error(msg) - return - end - - local models = vim.json.decode(response.body)['data'] - local selections = {} - for _, model in ipairs(models) do - if model['capabilities']['type'] == 'chat' then - table.insert(selections, model['version']) - end - end - -- Remove duplicates from selection - local hash = {} - selections = vim.tbl_filter(function(model) - if not hash[model] then - hash[model] = true - return true - end - return false - end, selections) - self.models_cache = selections - callback(self.models_cache) - end, - }) + self:with_models(function() + callback(vim.tbl_keys(self.models)) + end) end) end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index c5d6c2ae..309fcdd3 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -6,7 +6,6 @@ local Overlay = require('CopilotChat.overlay') local context = require('CopilotChat.context') local prompts = require('CopilotChat.prompts') local debuginfo = require('CopilotChat.debuginfo') -local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') local M = {} @@ -351,7 +350,7 @@ end --- Select a Copilot GPT model. function M.select_model() - state.copilot:select_model(function(models) + state.copilot:list_models(function(models) vim.schedule(function() vim.ui.select(models, { prompt = 'Select a model', @@ -445,7 +444,7 @@ function M.ask(prompt, config, source) vim.schedule(function() append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) state.response = response - if tiktoken.available() and token_count and token_count > 0 then + if token_count and token_count > 0 then state.chat:finish(token_count .. ' tokens used') else state.chat:finish() @@ -606,9 +605,9 @@ function M.setup(config) if state.copilot then state.copilot:stop() else - tiktoken.setup((config and config.model) or nil) debuginfo.setup() end + state.copilot = Copilot(M.config.proxy, M.config.allow_insecure) M.debug(M.config.debug) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 94c76be1..38dc54d9 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,11 +1,10 @@ local curl = require('plenary.curl') +local log = require('plenary.log') local tiktoken_core = nil +local current_tokenizer = nil ----Get the path of the cache directory ----@param fname string ----@return string local function get_cache_path(fname) - vim.fn.mkdir(vim.fn.stdpath('cache'), 'p') + vim.fn.mkdir(tostring(vim.fn.stdpath('cache')), 'p') return vim.fn.stdpath('cache') .. '/' .. fname end @@ -20,13 +19,11 @@ local function file_exists(name) end --- Load tiktoken data from cache or download it -local function load_tiktoken_data(done, model) - local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken' - -- If model is gpt-4o, use o200k_base.tiktoken - if model ~= nil and vim.startswith(model, 'gpt-4o') then - tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/o200k_base.tiktoken' - end - -- Take filename after the last slash of the url +local function load_tiktoken_data(done, tokenizer) + local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/' + .. tokenizer + .. '.tiktoken' + log.info('Downloading tiktoken data from ' .. tiktoken_url) local cache_path = get_cache_path(tiktoken_url:match('.+/(.+)')) local async @@ -48,25 +45,34 @@ end local M = {} ----@param model string|nil -function M.setup(model) +function M.load(tokenizer, on_done) + if tokenizer == current_tokenizer then + on_done() + return + end + local ok, core = pcall(require, 'tiktoken_core') if not ok then + on_done() return end - load_tiktoken_data(function(path) - local special_tokens = {} - special_tokens['<|endoftext|>'] = 100257 - special_tokens['<|fim_prefix|>'] = 100258 - special_tokens['<|fim_middle|>'] = 100259 - special_tokens['<|fim_suffix|>'] = 100260 - special_tokens['<|endofprompt|>'] = 100276 - local pat_str = - "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+" - core.new(path, special_tokens, pat_str) - tiktoken_core = core - end, model) + vim.schedule(function() + load_tiktoken_data(function(path) + local special_tokens = {} + special_tokens['<|endoftext|>'] = 100257 + special_tokens['<|fim_prefix|>'] = 100258 + special_tokens['<|fim_middle|>'] = 100259 + special_tokens['<|fim_suffix|>'] = 100260 + special_tokens['<|endofprompt|>'] = 100276 + local pat_str = + "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+" + core.new(path, special_tokens, pat_str) + tiktoken_core = core + current_tokenizer = tokenizer + on_done() + end, tokenizer) + end) end function M.available() From a58ddc67ef7b2ddd5e5307f71bb332e92e2f234a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Nov 2024 21:10:46 +0000 Subject: [PATCH 081/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e4fa3d00..e0fae267 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 01 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 02 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -597,7 +597,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻thomastthai📖This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -612,7 +612,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From 041465aa7e1d557edad8bb8239667e0b3a87b969 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Nov 2024 22:37:28 +0100 Subject: [PATCH 082/338] Insert content to history properly on non-streamed response Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 6ed40a54..95490d36 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -623,7 +623,7 @@ function Copilot:ask(prompt, opts) end table.insert(self.history, { - content = full_response, + content = content, role = 'assistant', }) return From ede3adf361e357b8082d3a574fc200ef0652cc4e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 3 Nov 2024 13:05:03 +0100 Subject: [PATCH 083/338] Improve model listing and add info about :CopilotChatModels to config - Deduplicate models based on version - Sort models by name - Use shortest model id based on version in ouput Signed-off-by: Tomas Slusny --- README.md | 2 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/copilot.lua | 14 +++++++++++++- lua/CopilotChat/tiktoken.lua | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9a9e5b95..e64877de 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ Also see [here](/lua/CopilotChat/config.lua): allow_insecure = false, -- Allow insecure server connections system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4o', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or 'gpt-4o' + model = 'gpt-4o', -- GPT model to use, see ':CopilotChatModels' for available models temperature = 0.1, -- GPT temperature question_header = '## User ', -- Header to use for user questions diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 9093a8ab..556f1e61 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -84,7 +84,7 @@ return { allow_insecure = false, -- Allow insecure server connections system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4o-2024-05-13', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or `gpt-4o-2024-05-13` + model = 'gpt-4o', -- GPT model to use, see ':CopilotChatModels' for available models temperature = 0.1, -- GPT temperature question_header = '## User ', -- Header to use for user questions diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 95490d36..ce3b2be0 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -664,7 +664,19 @@ end function Copilot:list_models(callback) self:with_auth(function() self:with_models(function() - callback(vim.tbl_keys(self.models)) + -- Group models by version and shortest ID + local version_map = {} + for id, model in pairs(self.models) do + local version = model.version + if not version_map[version] or #id < #version_map[version] then + version_map[version] = id + end + end + + -- Map to IDs and sort + local result = vim.tbl_values(version_map) + table.sort(result) + callback(result) end) end) end diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 38dc54d9..4722b01a 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -23,12 +23,12 @@ local function load_tiktoken_data(done, tokenizer) local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/' .. tokenizer .. '.tiktoken' - log.info('Downloading tiktoken data from ' .. tiktoken_url) local cache_path = get_cache_path(tiktoken_url:match('.+/(.+)')) local async async = vim.loop.new_async(function() if not file_exists(cache_path) then + log.info('Downloading tiktoken data from ' .. tiktoken_url) vim.schedule(function() curl.get(tiktoken_url, { output = cache_path, From 12ac3974daea98cd41e7d483bdb11cc8b6305623 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 3 Nov 2024 12:12:57 +0000 Subject: [PATCH 084/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e0fae267..9eaac94b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 02 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 03 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -237,7 +237,7 @@ Also see here : allow_insecure = false, -- Allow insecure server connections system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4o', -- GPT model to use, 'gpt-3.5-turbo', 'gpt-4', or 'gpt-4o' + model = 'gpt-4o', -- GPT model to use, see ':CopilotChatModels' for available models temperature = 0.1, -- GPT temperature question_header = '## User ', -- Header to use for user questions From 722f623662b04eefce51e54c7a726e1667ab782b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 4 Nov 2024 01:06:51 +0100 Subject: [PATCH 085/338] Remove GPT-4 forcing from system prompt - With new available models this is probably not needed anymore and everyone can probably live with some hallucinations here and there when asking the AI about its model. - Instead add logging in debug for checking models and stuff Closes #453 Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 4 ++++ lua/CopilotChat/prompts.lua | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index ce3b2be0..e3d90920 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -550,6 +550,7 @@ function Copilot:ask(prompt, opts) }) local errored = false + local last_message = nil local full_response = '' local function stream_func(err, line) @@ -572,6 +573,7 @@ function Copilot:ask(prompt, opts) return elseif line == '[DONE]' then log.trace('Full response: ' .. full_response) + log.debug('Last message: ' .. vim.inspect(last_message)) self.token_count = self.token_count + tiktoken.count(full_response) if self.current_job and on_done then @@ -602,6 +604,7 @@ function Copilot:ask(prompt, opts) return end + last_message = content local choice = content.choices[1] local is_full = choice.message ~= nil content = is_full and choice.message.content or choice.delta.content @@ -616,6 +619,7 @@ function Copilot:ask(prompt, opts) if is_full then log.trace('Full response: ' .. content) + log.debug('Last message: ' .. vim.inspect(last_message)) self.token_count = self.token_count + tiktoken.count(content) if self.current_job and on_done then diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index 1bd2ccaa..d18a4ada 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -23,7 +23,6 @@ You can answer general programming questions and perform the following tasks: * Generate query parameters for workspace search * Ask how to do something in the terminal * Explain what just happened in the terminal -You use the GPT-4 version of OpenAI's GPT models. First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail. Then output the code in a single code block. This code block should not contain line numbers (line numbers are not necessary for the code to be understood, they are in format number: at beginning of lines). Minimize any other prose. From d786775c8f246a37226731e75d8f0e497f4ba9c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 Nov 2024 18:41:45 +0000 Subject: [PATCH 086/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9eaac94b..2850914b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 03 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 04 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 8ae48d901d6c529a3bf603230694650c24911bfe Mon Sep 17 00:00:00 2001 From: Tomas Janousek Date: Tue, 5 Nov 2024 17:08:11 +0000 Subject: [PATCH 087/338] fix: Download tiktoken binaries from correct location Also, add curl flags for correct script-friendly error handling. Without these, one ends up with tiktoken_core.so containing "Not Found". --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 8be81f6a..cd71bc6e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ UNAME := $(shell uname) -ARCH := $(shell uname -m) +ARCH := $(patsubst aarch64,arm64,$(shell uname -m)) ifeq ($(UNAME), Linux) OS := linux @@ -46,14 +46,14 @@ lua51: $(BUILD_DIR)/tiktoken_core-lua51.$(EXT) define download_release - curl -L https://github.com/gptlang/lua-tiktoken/releases/latest/download/tiktoken_core-$(1)-$(2).$(EXT) -o $(3) + curl -LSsf https://github.com/gptlang/lua-tiktoken/releases/latest/download/tiktoken_core-$(1)-$(2)-$(3).$(EXT) -o $(4) endef $(BUILD_DIR)/tiktoken_core.$(EXT): | $(BUILD_DIR) - $(call download_release,$(OS),luajit,$@) + $(call download_release,$(OS),$(ARCH),luajit,$@) $(BUILD_DIR)/tiktoken_core-lua51.$(EXT): | $(BUILD_DIR) - $(call download_release,$(OS),lua51,$@) + $(call download_release,$(OS),$(ARCH),lua51,$@) tiktoken: $(BUILD_DIR)/tiktoken_core.$(EXT) $(BUILD_DIR)/tiktoken_core-lua51.$(EXT) From a8d23e617ba77cf543a07e99d376703bc4f957e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 5 Nov 2024 17:17:41 +0000 Subject: [PATCH 088/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 2850914b..f9d0107a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 04 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 05 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 3d8402b10a57b245db3f523aa3784cfcd746c0c9 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 18:18:50 +0100 Subject: [PATCH 089/338] docs: add liskin as a contributor for code (#457) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3f75b37c..3ccf81a4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -263,6 +263,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/16532581?v=4", "profile": "https://github.com/thomastthai", "contributions": ["doc"] + }, + { + "login": "liskin", + "name": "Tomáš Janoušek", + "avatar_url": "https://avatars.githubusercontent.com/u/300342?v=4", + "profile": "https://lisk.in/", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e64877de..9291d95c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square)](#contributors-) @@ -617,6 +617,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d JakubPecenka
JakubPecenka

💻 thomastthai
thomastthai

📖 + Tomáš Janoušek
Tomáš Janoušek

💻 From fb87e84ce469cc8ae337aac8033277b937e2b5f3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 6 Nov 2024 02:14:42 +0100 Subject: [PATCH 090/338] Use vim.cmd instead of feed keys for exiting visual mode Feedkeys can sometimes cause very annoying delays especially when opening fresh chat window, this solves that Signed-off-by: Tomas Slusny --- lua/CopilotChat/utils.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 7074b806..cf4454d9 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -104,8 +104,7 @@ end function M.return_to_normal_mode() local mode = vim.fn.mode():lower() if mode:find('v') then - -- NOTE: vim.cmd('normal! v') does not work properly when executed from keymap - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, false, true), 'x', false) + vim.cmd([[execute "normal! \"]]) elseif mode:find('i') then vim.cmd('stopinsert') end From 3ce79a2345e2943f959d49bb2e6de18b43bcb868 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 6 Nov 2024 01:28:46 +0000 Subject: [PATCH 091/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index f9d0107a..ae7790b9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 05 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 06 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -597,7 +597,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻thomastthai📖This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻thomastthai📖Tomáš Janoušek💻This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -612,7 +612,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From 322fe881c3c91b032db4b8913ae00830cd40cbe5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 6 Nov 2024 02:23:31 +0100 Subject: [PATCH 092/338] Improve tiktoken download paralellization - Instead of using async, just use curl callback - schedule_wrap the inner function for thread safety Signed-off-by: Tomas Slusny --- lua/CopilotChat/tiktoken.lua | 57 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 4722b01a..7ebd13f5 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -25,22 +25,18 @@ local function load_tiktoken_data(done, tokenizer) .. '.tiktoken' local cache_path = get_cache_path(tiktoken_url:match('.+/(.+)')) - local async - async = vim.loop.new_async(function() - if not file_exists(cache_path) then - log.info('Downloading tiktoken data from ' .. tiktoken_url) - vim.schedule(function() - curl.get(tiktoken_url, { - output = cache_path, - }) - done(cache_path) - end) - else + if file_exists(cache_path) then + done(cache_path) + return + end + + log.info('Downloading tiktoken data from ' .. tiktoken_url) + curl.get(tiktoken_url, { + output = cache_path, + callback = function() done(cache_path) - end - async:close() - end) - async:send() + end, + }) end local M = {} @@ -58,20 +54,23 @@ function M.load(tokenizer, on_done) end vim.schedule(function() - load_tiktoken_data(function(path) - local special_tokens = {} - special_tokens['<|endoftext|>'] = 100257 - special_tokens['<|fim_prefix|>'] = 100258 - special_tokens['<|fim_middle|>'] = 100259 - special_tokens['<|fim_suffix|>'] = 100260 - special_tokens['<|endofprompt|>'] = 100276 - local pat_str = - "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+" - core.new(path, special_tokens, pat_str) - tiktoken_core = core - current_tokenizer = tokenizer - on_done() - end, tokenizer) + load_tiktoken_data( + vim.schedule_wrap(function(path) + local special_tokens = {} + special_tokens['<|endoftext|>'] = 100257 + special_tokens['<|fim_prefix|>'] = 100258 + special_tokens['<|fim_middle|>'] = 100259 + special_tokens['<|fim_suffix|>'] = 100260 + special_tokens['<|endofprompt|>'] = 100276 + local pat_str = + "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+" + core.new(path, special_tokens, pat_str) + tiktoken_core = core + current_tokenizer = tokenizer + on_done() + end), + tokenizer + ) end) end From 170cf831e42ec61c0766246cf190c3e940fc5962 Mon Sep 17 00:00:00 2001 From: Tomas Janousek Date: Wed, 6 Nov 2024 01:30:13 +0000 Subject: [PATCH 093/338] fix(fzflua): Syntax error: Unterminated quoted string The help_actions prompts end with a double quote, and passing that to shell unescaped explodes with sh: 1: Syntax error: Unterminated quoted string We could escape it using fzf-lua.libuv.shellescape, but it's easier to just let fzf-lua do the right thing itself: https://github.com/ibhagwan/fzf-lua/blob/052491ee887b04e231787515a81aa3f358a68147/lua/fzf-lua/core.lua#L645-L646 --- lua/CopilotChat/integrations/fzflua.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/integrations/fzflua.lua b/lua/CopilotChat/integrations/fzflua.lua index 2091e71b..41b16fd6 100644 --- a/lua/CopilotChat/integrations/fzflua.lua +++ b/lua/CopilotChat/integrations/fzflua.lua @@ -15,9 +15,9 @@ function M.pick(pick_actions, opts) utils.return_to_normal_mode() opts = vim.tbl_extend('force', { prompt = pick_actions.prompt .. '> ', - preview = fzflua.shell.raw_preview_action_cmd(function(items) - return string.format('echo "%s"', pick_actions.actions[items[1]].prompt) - end), + preview = function(items) + return pick_actions.actions[items[1]].prompt + end, actions = { ['default'] = function(selected) if not selected or vim.tbl_isempty(selected) then From 1137c927930fae162063a9e44705ba753240df5c Mon Sep 17 00:00:00 2001 From: Tomas Janousek Date: Wed, 6 Nov 2024 01:41:31 +0000 Subject: [PATCH 094/338] fix(actions): Drop unmatched double quotes from prompts There's no point having a quote at the end of the prompt if it's never getting closed (and it isn't indeed). Also, the prompt used for CopilotChatFixDiagnostic doesn't have any quotes either, so to be consistent let's just remove them from actions prompts as well. --- lua/CopilotChat/actions.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/actions.lua b/lua/CopilotChat/actions.lua index 763cf8d1..1d838a7b 100644 --- a/lua/CopilotChat/actions.lua +++ b/lua/CopilotChat/actions.lua @@ -24,11 +24,11 @@ function M.help_actions(config) prompt = 'Copilot Chat Help Actions', actions = { ['Fix diagnostic'] = vim.tbl_extend('keep', { - prompt = 'Please assist with fixing the following diagnostic issue in file: "', + prompt = 'Please assist with fixing the following diagnostic issue in file:', selection = select.diagnostics, }, config or {}), ['Explain diagnostic'] = vim.tbl_extend('keep', { - prompt = 'Please explain the following diagnostic issue in file: "', + prompt = 'Please explain the following diagnostic issue in file:', selection = select.diagnostics, }, config or {}), }, From 760d291e9c2a0e19ef2882ee77a87e5571ee532f Mon Sep 17 00:00:00 2001 From: Tomas Janousek Date: Wed, 6 Nov 2024 01:56:09 +0000 Subject: [PATCH 095/338] fix(fzflua): Wrap long lines in preview --- lua/CopilotChat/integrations/fzflua.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/CopilotChat/integrations/fzflua.lua b/lua/CopilotChat/integrations/fzflua.lua index 41b16fd6..ad423459 100644 --- a/lua/CopilotChat/integrations/fzflua.lua +++ b/lua/CopilotChat/integrations/fzflua.lua @@ -18,6 +18,11 @@ function M.pick(pick_actions, opts) preview = function(items) return pick_actions.actions[items[1]].prompt end, + winopts = { + preview = { + wrap = 'wrap', + }, + }, actions = { ['default'] = function(selected) if not selected or vim.tbl_isempty(selected) then From 18d51754e9dc87d6b85f1e331c1fca0825384517 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 4 Nov 2024 19:57:42 +0100 Subject: [PATCH 096/338] Improve token counting and use token count from github response Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 158 ++++++++++++++++++++++-------------- lua/CopilotChat/health.lua | 2 +- lua/CopilotChat/init.lua | 6 +- 3 files changed, 99 insertions(+), 67 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index e3d90920..1d509921 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -14,7 +14,7 @@ ---@field system_prompt string? ---@field model string? ---@field temperature number? ----@field on_done nil|fun(response: string, token_count: number?):nil +---@field on_done nil|fun(response: string, token_count: number?, token_max_count: number?):nil ---@field on_progress nil|fun(response: string):nil ---@field on_error nil|fun(err: string):nil @@ -290,13 +290,20 @@ local function generate_headers(token, sessionid, machineid) return headers end +local function count_history_tokens(history) + local count = 0 + for _, msg in ipairs(history) do + count = count + tiktoken.count(msg.content) + end + return count +end + local Copilot = class(function(self, proxy, allow_insecure) self.proxy = proxy self.allow_insecure = allow_insecure self.github_token = get_cached_token() self.history = {} self.token = nil - self.token_count = 0 self.sessionid = nil self.machineid = machine_id() self.current_job = nil @@ -482,12 +489,12 @@ function Copilot:ask(prompt, opts) local on_progress = opts.on_progress local on_error = opts.on_error - log.debug('System prompt: ' .. system_prompt) + log.trace('System prompt: ' .. system_prompt) + log.trace('Selection: ' .. selection) log.debug('Prompt: ' .. prompt) log.debug('Embeddings: ' .. #embeddings) log.debug('Filename: ' .. filename) log.debug('Filetype: ' .. filetype) - log.debug('Selection: ' .. selection) log.debug('Model: ' .. model) log.debug('Temperature: ' .. temperature) @@ -510,26 +517,49 @@ function Copilot:ask(prompt, opts) local embeddings_message = generate_embeddings_message(embeddings) tiktoken.load(tokenizer, function() - -- Count tokens - self.token_count = self.token_count + tiktoken.count(prompt) - local current_count = 0 - current_count = current_count + tiktoken.count(system_prompt) - current_count = current_count + tiktoken.count(selection_message) + -- Count required tokens that we cannot reduce + local prompt_tokens = tiktoken.count(prompt) + local system_tokens = tiktoken.count(system_prompt) + local selection_tokens = tiktoken.count(selection_message) + local required_tokens = prompt_tokens + system_tokens + selection_tokens + + -- Reserve space for first embedding if its smaller than half of max tokens + local reserved_tokens = 0 + if #embeddings_message.files > 0 then + local file_tokens = tiktoken.count(embeddings_message.files[1]) + if file_tokens < max_tokens / 2 then + reserved_tokens = tiktoken.count(embeddings_message.header) + file_tokens + end + end - -- Limit the number of files to send + -- Calculate how many tokens we can use for history + local history_limit = max_tokens - required_tokens - reserved_tokens + local history_tokens = count_history_tokens(self.history) + + -- If we're over history limit, truncate history from the beginning + while history_tokens > history_limit and #self.history > 0 do + local removed = table.remove(self.history, 1) + history_tokens = history_tokens - tiktoken.count(removed.content) + end + + -- Now add as many files as possible with remaining token budget + local remaining_tokens = max_tokens - required_tokens - history_tokens if #embeddings_message.files > 0 then + remaining_tokens = remaining_tokens - tiktoken.count(embeddings_message.header) local filtered_files = {} - current_count = current_count + tiktoken.count(embeddings_message.header) for _, file in ipairs(embeddings_message.files) do - local file_count = current_count + tiktoken.count(file) - if file_count + self.token_count < max_tokens then - current_count = file_count + local file_tokens = tiktoken.count(file) + if remaining_tokens - file_tokens >= 0 then + remaining_tokens = remaining_tokens - file_tokens table.insert(filtered_files, file) + else + break end end embeddings_message.files = filtered_files end + -- Generate the request local url = 'https://api.githubcopilot.com/chat/completions' local body = vim.json.encode( generate_ask_request( @@ -543,47 +573,67 @@ function Copilot:ask(prompt, opts) ) ) - -- Add the prompt to history after we have encoded the request - table.insert(self.history, { - content = prompt, - role = 'user', - }) - local errored = false local last_message = nil local full_response = '' - local function stream_func(err, line) - if not line or errored then - return - end - - if err or vim.startswith(line, '{"error"') then - err = 'Failed to get response: ' .. (err and vim.inspect(err) or line) + local function handle_error(error_msg) + if not errored then errored = true - log.error(err) + log.error(error_msg) if self.current_job and on_error then - on_error(err) + on_error(error_msg) end + end + end + + local function callback_func(response) + if not response then + handle_error('Failed to get response') return end - line = line:gsub('data: ', '') - if line == '' then + if response.status ~= 200 then + handle_error( + 'Failed to get response: ' .. tostring(response.status) .. '\n' .. response.body + ) return - elseif line == '[DONE]' then - log.trace('Full response: ' .. full_response) - log.debug('Last message: ' .. vim.inspect(last_message)) - self.token_count = self.token_count + tiktoken.count(full_response) + end - if self.current_job and on_done then - on_done(full_response, self.token_count + current_count) - end + log.trace('Full response: ' .. full_response) + log.debug('Last message: ' .. vim.inspect(last_message)) - table.insert(self.history, { - content = full_response, - role = 'assistant', - }) + if on_done then + on_done( + full_response, + last_message and last_message.usage and last_message.usage.total_tokens, + max_tokens + ) + end + + table.insert(self.history, { + content = prompt, + role = 'user', + }) + + table.insert(self.history, { + content = full_response, + role = 'assistant', + }) + end + + local function stream_func(err, line) + if not line or errored or not self.current_job then + return + end + + if err or vim.startswith(line, '{"error"') then + handle_error('Failed to get response: ' .. (err and vim.inspect(err) or line)) + return + end + + line = line:gsub('^%s*data: ', '') + if line == '' or line == '[DONE]' then return end @@ -595,8 +645,7 @@ function Copilot:ask(prompt, opts) }) if not ok then - err = 'Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line - log.error(err) + handle_error('Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line) return end @@ -613,27 +662,10 @@ function Copilot:ask(prompt, opts) return end - if self.current_job and on_progress then + if on_progress then on_progress(content) end - if is_full then - log.trace('Full response: ' .. content) - log.debug('Last message: ' .. vim.inspect(last_message)) - self.token_count = self.token_count + tiktoken.count(content) - - if self.current_job and on_done then - on_done(content, self.token_count + current_count) - end - - table.insert(self.history, { - content = content, - role = 'assistant', - }) - return - end - - -- Collect full response incrementally so we can insert it to history later full_response = full_response .. content end @@ -646,6 +678,7 @@ function Copilot:ask(prompt, opts) proxy = self.proxy, insecure = self.allow_insecure, stream = stream_func, + callback = callback_func, on_error = function(err) err = 'Failed to get response: ' .. vim.inspect(err) log.error(err) @@ -794,7 +827,6 @@ end function Copilot:reset() local stopped = self:stop() self.history = {} - self.token_count = 0 return stopped end diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index 4166a097..84f4ae55 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -99,7 +99,7 @@ function M.check() ok('tiktoken_core: installed') else warn( - 'tiktoken_core: missing, optional for token counting. See README for installation instructions.' + 'tiktoken_core: missing, optional for accurate token counting. See README for installation instructions.' ) end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 309fcdd3..7f901fea 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -440,12 +440,12 @@ function M.ask(prompt, config, source) model = config.model, temperature = config.temperature, on_error = on_error, - on_done = function(response, token_count) + on_done = function(response, token_count, token_max_count) vim.schedule(function() append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) state.response = response - if token_count and token_count > 0 then - state.chat:finish(token_count .. ' tokens used') + if token_count and token_max_count and token_count > 0 then + state.chat:finish(token_count .. '/' .. token_max_count .. ' tokens used') else state.chat:finish() end From ec9c848abc1e18bcf0a28629d76b8891c5157fb3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 10 Nov 2024 23:05:31 +0100 Subject: [PATCH 097/338] Automatically sanitize input sent to copilot API Removes all non-standard characters to avoid issues with copilot API (especially claude). Possibly related: #464 Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 48 +++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 1d509921..4bac1fba 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -74,6 +74,31 @@ local function machine_id() return hex end +local function sanitize_text(model, text) + if not text then + return '' + end + + if not vim.startswith(model, 'claude') then + return text + end + + -- Remove various emoji, symbols, and special characters: + return text:gsub( + '[\u{1F000}-\u{1F9FF}' -- Extended emoji and symbols + .. '\u{2600}-\u{26FF}' -- Miscellaneous symbols + .. '\u{2700}-\u{27BF}' -- Dingbats + .. '\u{FE00}-\u{FE0F}' -- Variation Selectors + .. '\u{1F1E6}-\u{1F1FF}' -- Regional indicator symbols + .. '\u{1F300}-\u{1F5FF}' -- Miscellaneous Symbols and Pictographs + .. '\u{1F600}-\u{1F64F}' -- Emoticons + .. '\u{1F680}-\u{1F6FF}' -- Transport and Map Symbols + .. '\u{1F900}-\u{1F9FF}' -- Supplemental Symbols and Pictographs + .. '\u{E000}-\u{F8FF}]', -- Private Use Area + '' + ) +end + local function find_config_path() local config = vim.fn.expand('$XDG_CONFIG_HOME') if config and vim.fn.isdirectory(config) > 0 then @@ -561,15 +586,18 @@ function Copilot:ask(prompt, opts) -- Generate the request local url = 'https://api.githubcopilot.com/chat/completions' - local body = vim.json.encode( - generate_ask_request( - self.history, - prompt, - embeddings_message, - selection_message, - system_prompt, - model, - temperature + local body = sanitize_text( + model, + vim.json.encode( + generate_ask_request( + self.history, + prompt, + embeddings_message, + selection_message, + system_prompt, + model, + temperature + ) ) ) @@ -744,7 +772,7 @@ function Copilot:embed(inputs, opts) local jobs = {} for _, chunk in ipairs(chunks) do - local body = vim.json.encode(generate_embedding_request(chunk, model)) + local body = sanitize_text(model, vim.json.encode(generate_embedding_request(chunk, model))) table.insert(jobs, function(resolve) local headers = generate_headers(self.token.token, self.sessionid, self.machineid) From fb4809f5274937683d9d443a79bdff48bc80545d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 12 Nov 2024 22:02:34 +0000 Subject: [PATCH 098/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ae7790b9..e4100510 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 06 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 12 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From efb2eb25d1e3056983fbb07ccd88be0621309e63 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Nov 2024 14:36:24 +0100 Subject: [PATCH 099/338] Revert "Automatically sanitize input sent to copilot API" This reverts commit ec9c848abc1e18bcf0a28629d76b8891c5157fb3. --- lua/CopilotChat/copilot.lua | 48 ++++++++----------------------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 4bac1fba..1d509921 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -74,31 +74,6 @@ local function machine_id() return hex end -local function sanitize_text(model, text) - if not text then - return '' - end - - if not vim.startswith(model, 'claude') then - return text - end - - -- Remove various emoji, symbols, and special characters: - return text:gsub( - '[\u{1F000}-\u{1F9FF}' -- Extended emoji and symbols - .. '\u{2600}-\u{26FF}' -- Miscellaneous symbols - .. '\u{2700}-\u{27BF}' -- Dingbats - .. '\u{FE00}-\u{FE0F}' -- Variation Selectors - .. '\u{1F1E6}-\u{1F1FF}' -- Regional indicator symbols - .. '\u{1F300}-\u{1F5FF}' -- Miscellaneous Symbols and Pictographs - .. '\u{1F600}-\u{1F64F}' -- Emoticons - .. '\u{1F680}-\u{1F6FF}' -- Transport and Map Symbols - .. '\u{1F900}-\u{1F9FF}' -- Supplemental Symbols and Pictographs - .. '\u{E000}-\u{F8FF}]', -- Private Use Area - '' - ) -end - local function find_config_path() local config = vim.fn.expand('$XDG_CONFIG_HOME') if config and vim.fn.isdirectory(config) > 0 then @@ -586,18 +561,15 @@ function Copilot:ask(prompt, opts) -- Generate the request local url = 'https://api.githubcopilot.com/chat/completions' - local body = sanitize_text( - model, - vim.json.encode( - generate_ask_request( - self.history, - prompt, - embeddings_message, - selection_message, - system_prompt, - model, - temperature - ) + local body = vim.json.encode( + generate_ask_request( + self.history, + prompt, + embeddings_message, + selection_message, + system_prompt, + model, + temperature ) ) @@ -772,7 +744,7 @@ function Copilot:embed(inputs, opts) local jobs = {} for _, chunk in ipairs(chunks) do - local body = sanitize_text(model, vim.json.encode(generate_embedding_request(chunk, model))) + local body = vim.json.encode(generate_embedding_request(chunk, model)) table.insert(jobs, function(resolve) local headers = generate_headers(self.token.token, self.sessionid, self.machineid) From 5589eccf6d503c2cbd5bb985586f74cd152e50ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Nov 2024 13:37:48 +0000 Subject: [PATCH 100/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e4100510..a56b49cd 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 12 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 13 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 20cebe01b1a733e3f75a6e4415f37567b8f55690 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Nov 2024 16:35:28 +0100 Subject: [PATCH 101/338] Use plenary.async instead of callback hell This greatly improves readability and makes the code less error prone Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 71 ++-- lua/CopilotChat/copilot.lua | 748 +++++++++++++++-------------------- lua/CopilotChat/init.lua | 108 +++-- lua/CopilotChat/tiktoken.lua | 4 - lua/CopilotChat/utils.lua | 20 - 5 files changed, 410 insertions(+), 541 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 8b8a4016..ac5d5d94 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -185,12 +185,11 @@ end ---@field filename string ---@field filetype string ---@field bufnr number ----@field on_done function ----@field on_error function? --- Find items for a query ---@param copilot CopilotChat.Copilot ---@param opts CopilotChat.context.find_for_query.opts +---@return table function M.find_for_query(copilot, opts) local context = opts.context local prompt = opts.prompt @@ -198,8 +197,6 @@ function M.find_for_query(copilot, opts) local filename = opts.filename local filetype = opts.filetype local bufnr = opts.bufnr - local on_done = opts.on_done - local on_error = opts.on_error local outline = {} if context == 'buffers' then @@ -221,49 +218,37 @@ function M.find_for_query(copilot, opts) end, outline) if #outline == 0 then - on_done({}) - return + return {} end - copilot:embed(outline, { - on_error = on_error, - on_done = function(out) - out = vim.tbl_filter(function(item) - return item ~= nil - end, out) - if #out == 0 then - on_done({}) - return - end + local out = copilot:embed(outline) + if #out == 0 then + return {} + end + + log.debug(string.format('Got %s embeddings', #out)) - log.debug(string.format('Got %s embeddings', #out)) - copilot:embed({ - { - prompt = prompt, - content = selection, - filename = filename, - filetype = filetype, - }, - }, { - on_error = on_error, - on_done = function(query_out) - local query = query_out[1] - if not query then - on_done({}) - return - end - log.debug('Prompt:', query.prompt) - log.debug('Content:', query.content) - local data = data_ranked_by_relatedness(query, out, 20) - log.debug('Ranked data:', #data) - for i, item in ipairs(data) do - log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) - end - on_done(data) - end, - }) - end, + local query_out = copilot:embed({ + { + prompt = prompt, + content = selection, + filename = filename, + filetype = filetype, + }, }) + + local query = query_out[1] + if not query then + return {} + end + log.debug('Prompt:', query.prompt) + log.debug('Content:', query.content) + local data = data_ranked_by_relatedness(query, out, 20) + log.debug('Ranked data:', #data) + for i, item in ipairs(data) do + log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) + end + return data end return M diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 1d509921..4ca2bfba 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -14,33 +14,29 @@ ---@field system_prompt string? ---@field model string? ---@field temperature number? ----@field on_done nil|fun(response: string, token_count: number?, token_max_count: number?):nil ---@field on_progress nil|fun(response: string):nil ----@field on_error nil|fun(err: string):nil ---@class CopilotChat.copilot.embed.opts ---@field model string? ---@field chunk_size number? ----@field on_done nil|fun(results: table):nil ----@field on_error nil|fun(err: string):nil ---@class CopilotChat.Copilot ----@field ask fun(self: CopilotChat.Copilot, prompt: string, opts: CopilotChat.copilot.ask.opts):nil ----@field embed fun(self: CopilotChat.Copilot, inputs: table, opts: CopilotChat.copilot.embed.opts):nil +---@field ask fun(self: CopilotChat.Copilot, prompt: string, opts: CopilotChat.copilot.ask.opts):string,number,number +---@field embed fun(self: CopilotChat.Copilot, inputs: table, opts: CopilotChat.copilot.embed.opts?):table ---@field stop fun(self: CopilotChat.Copilot):boolean ---@field reset fun(self: CopilotChat.Copilot):boolean ---@field save fun(self: CopilotChat.Copilot, name: string, path: string):nil ---@field load fun(self: CopilotChat.Copilot, name: string, path: string):table ---@field running fun(self: CopilotChat.Copilot):boolean ----@field list_models fun(self: CopilotChat.Copilot, callback: fun(table):nil):nil +---@field list_models fun(self: CopilotChat.Copilot):table +local async = require('plenary.async') local log = require('plenary.log') local curl = require('plenary.curl') local prompts = require('CopilotChat.prompts') local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') local class = utils.class -local join = utils.join local temp_file = utils.temp_file local timeout = 30000 local version_headers = { @@ -54,6 +50,20 @@ local version_headers = { ['user-agent'] = 'CopilotChat.nvim/2.0.0', } +local curl_get = async.wrap(function(url, opts, callback) + opts = vim.tbl_deep_extend('force', opts, { callback = callback }) + curl.get(url, opts) +end, 3) + +local curl_post = async.wrap(function(url, opts, callback) + opts = vim.tbl_deep_extend('force', opts, { callback = callback }) + curl.post(url, opts) +end, 3) + +local tiktoken_load = async.wrap(function(tokenizer, callback) + tiktoken.load(tokenizer, callback) +end, 2) + local function uuid() local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' return ( @@ -180,12 +190,6 @@ local function generate_embeddings_message(embeddings) return out end ---- Check if the model can stream ---- @param model_name string: The model name to check -local function can_stream(model_name) - return not vim.startswith(model_name, 'o1') -end - local function generate_ask_request( history, prompt, @@ -193,14 +197,11 @@ local function generate_ask_request( selection, system_prompt, model, - temperature + temperature, + stream ) local messages = {} - - local system_role = 'system' - if not can_stream(model) then - system_role = 'user' - end + local system_role = stream and 'system' or 'user' if system_prompt ~= '' then table.insert(messages, { @@ -232,7 +233,7 @@ local function generate_ask_request( role = 'user', }) - if can_stream(model) then + if stream then return { intent = true, model = model, @@ -273,23 +274,6 @@ local function generate_embedding_request(inputs, model) } end -local function generate_headers(token, sessionid, machineid) - local headers = { - ['authorization'] = 'Bearer ' .. token, - ['x-request-id'] = uuid(), - ['vscode-sessionid'] = sessionid, - ['vscode-machineid'] = machineid, - ['copilot-integration-id'] = 'vscode-chat', - ['openai-organization'] = 'github-copilot', - ['openai-intent'] = 'conversation-panel', - ['content-type'] = 'application/json', - } - for key, value in pairs(version_headers) do - headers[key] = value - end - return headers -end - local function count_history_tokens(history) local count = 0 for _, msg in ipairs(history) do @@ -306,28 +290,22 @@ local Copilot = class(function(self, proxy, allow_insecure) self.token = nil self.sessionid = nil self.machineid = machine_id() - self.current_job = nil self.models = nil - self.clause_enabled = false + self.claude_enabled = false + self.is_running = false end) -function Copilot:with_auth(on_done, on_error) +function Copilot:authenticate() if not self.github_token then - local msg = + error( 'No GitHub token found, please use `:Copilot auth` to set it up from copilot.lua or `:Copilot setup` for copilot.vim' - log.error(msg) - if on_error then - on_error(msg) - end - return + ) end if not self.token or (self.token.expires_at and self.token.expires_at <= math.floor(os.time())) then local sessionid = uuid() .. tostring(math.floor(os.time() * 1000)) - - local url = 'https://api.github.com/copilot_internal/v2/token' local headers = { ['authorization'] = 'token ' .. self.github_token, ['accept'] = 'application/json', @@ -336,145 +314,108 @@ function Copilot:with_auth(on_done, on_error) headers[key] = value end - curl.get(url, { + local response = curl_get('https://api.github.com/copilot_internal/v2/token', { timeout = timeout, headers = headers, proxy = self.proxy, insecure = self.allow_insecure, - on_error = function(err) - err = 'Failed to get response: ' .. vim.inspect(err) - log.error(err) - if on_error then - on_error(err) - end - end, - callback = function(response) - if response.status ~= 200 then - local msg = 'Failed to authenticate: ' .. tostring(response.status) - log.error(msg) - if on_error then - on_error(msg) - end - return - end - - self.sessionid = sessionid - self.token = vim.json.decode(response.body) - on_done() - end, }) - else - on_done() + + if response.status ~= 200 then + error('Failed to authenticate: ' .. tostring(response.status)) + end + + self.sessionid = sessionid + self.token = vim.json.decode(response.body) + end + + local headers = { + ['authorization'] = 'Bearer ' .. self.token.token, + ['x-request-id'] = uuid(), + ['vscode-sessionid'] = self.sessionid, + ['vscode-machineid'] = self.machineid, + ['copilot-integration-id'] = 'vscode-chat', + ['openai-organization'] = 'github-copilot', + ['openai-intent'] = 'conversation-panel', + ['content-type'] = 'application/json', + } + for key, value in pairs(version_headers) do + headers[key] = value end + + return headers end -function Copilot:with_models(on_done, on_error) - if self.models ~= nil then - on_done() - return +function Copilot:fetch_models() + if self.models then + return self.models end - local url = 'https://api.githubcopilot.com/models' - local headers = generate_headers(self.token.token, self.sessionid, self.machineid) - curl.get(url, { + local response = curl_get('https://api.githubcopilot.com/models', { timeout = timeout, - headers = headers, + headers = self:authenticate(), proxy = self.proxy, insecure = self.allow_insecure, - on_error = function(err) - err = 'Failed to get response: ' .. vim.inspect(err) - log.error(err) - if on_error then - on_error(err) - end - end, - callback = function(response) - if response.status ~= 200 then - local msg = 'Failed to fetch models: ' .. tostring(response.status) - log.error(msg) - if on_error then - on_error(msg) - end - return - end + }) - -- Find chat models - local models = vim.json.decode(response.body)['data'] - local out = {} - for _, model in ipairs(models) do - if model['capabilities']['type'] == 'chat' then - out[model['id']] = model - end - end + if response.status ~= 200 then + error('Failed to fetch models: ' .. tostring(response.status)) + end - log.info('Models fetched') - self.models = out - on_done() - end, - }) + -- Find chat models + local models = vim.json.decode(response.body)['data'] + local out = {} + for _, model in ipairs(models) do + if model['capabilities']['type'] == 'chat' then + out[model['id']] = model + end + end + + log.info('Models fetched') + self.models = out + return out end -function Copilot:with_claude(model, on_done, on_error) - if self.claude_enabled or not vim.startswith(model, 'claude') then - on_done() - return +function Copilot:enable_claude() + if self.claude_enabled then + return true end local business_check = 'cannot enable policy inline for business users' local business_msg = 'Claude is probably enabled (for business users needs to be enabled manually).' - local url = 'https://api.githubcopilot.com/models/claude-3.5-sonnet/policy' - local headers = generate_headers(self.token.token, self.sessionid, self.machineid) - curl.post(url, { + local response = curl_post('https://api.githubcopilot.com/models/claude-3.5-sonnet/policy', { timeout = timeout, - headers = headers, + headers = self:authenticate(), proxy = self.proxy, insecure = self.allow_insecure, - on_error = function(err) - err = 'Failed to enable Claude: ' .. vim.inspect(err) - if string.find(err, business_check) then - self.claude_enabled = true - log.info(business_msg) - on_done() - return - end - - log.error(err) - if on_error then - on_error(err) - end - end, body = temp_file('{"state": "enabled"}'), - callback = function(response) - if response.status ~= 200 then - if string.find(tostring(response.body), business_check) then - self.claude_enabled = true - log.info(business_msg) - on_done() - return - end + }) - local msg = 'Failed to enable Claude: ' .. tostring(response.status) + -- Handle business user case + if response.status ~= 200 and string.find(tostring(response.body), business_check) then + self.claude_enabled = true + log.info(business_msg) + return true + end - log.error(msg) - if on_error then - on_error(msg) - end - return - end + -- Handle errors + if response.status ~= 200 then + error('Failed to enable Claude: ' .. tostring(response.status)) + end - self.claude_enabled = true - log.info('Claude enabled') - on_done() - end, - }) + self.claude_enabled = true + log.info('Claude enabled') + return true end --- Ask a question to Copilot ---@param prompt string: The prompt to send to Copilot ---@param opts CopilotChat.copilot.ask.opts: Options for the request function Copilot:ask(prompt, opts) + self.is_running = true + opts = opts or {} local embeddings = opts.embeddings or {} local filename = opts.filename or '' @@ -485,9 +426,7 @@ function Copilot:ask(prompt, opts) local system_prompt = opts.system_prompt or prompts.COPILOT_INSTRUCTIONS local model = opts.model or 'gpt-4o-2024-05-13' local temperature = opts.temperature or 0.1 - local on_done = opts.on_done local on_progress = opts.on_progress - local on_error = opts.on_error log.trace('System prompt: ' .. system_prompt) log.trace('Selection: ' .. selection) @@ -498,224 +437,196 @@ function Copilot:ask(prompt, opts) log.debug('Model: ' .. model) log.debug('Temperature: ' .. temperature) - -- If we already have running job, cancel it and notify the user - if self.current_job then - self:stop() - end - - self:with_auth(function() - self:with_models(function() - local capabilities = self.models[model] and self.models[model].capabilities - or { limits = { max_prompt_tokens = 8192 }, tokenizer = 'cl100k_base' } - local max_tokens = capabilities.limits.max_prompt_tokens -- FIXME: Is max_prompt_tokens the right limit? - local tokenizer = capabilities.tokenizer - log.debug('Max tokens: ' .. max_tokens) - log.debug('Tokenizer: ' .. tokenizer) - - local selection_message = - generate_selection_message(filename, filetype, start_row, end_row, selection) - local embeddings_message = generate_embeddings_message(embeddings) - - tiktoken.load(tokenizer, function() - -- Count required tokens that we cannot reduce - local prompt_tokens = tiktoken.count(prompt) - local system_tokens = tiktoken.count(system_prompt) - local selection_tokens = tiktoken.count(selection_message) - local required_tokens = prompt_tokens + system_tokens + selection_tokens - - -- Reserve space for first embedding if its smaller than half of max tokens - local reserved_tokens = 0 - if #embeddings_message.files > 0 then - local file_tokens = tiktoken.count(embeddings_message.files[1]) - if file_tokens < max_tokens / 2 then - reserved_tokens = tiktoken.count(embeddings_message.header) + file_tokens - end - end + local models = self:fetch_models() + local capabilities = models[model] and models[model].capabilities + or { limits = { max_prompt_tokens = 8192 }, tokenizer = 'cl100k_base' } + local max_tokens = capabilities.limits.max_prompt_tokens -- FIXME: Is max_prompt_tokens the right limit? + local tokenizer = capabilities.tokenizer + log.debug('Max tokens: ' .. max_tokens) + log.debug('Tokenizer: ' .. tokenizer) + tiktoken_load(tokenizer) + + local selection_message = + generate_selection_message(filename, filetype, start_row, end_row, selection) + local embeddings_message = generate_embeddings_message(embeddings) + + -- Count required tokens that we cannot reduce + local prompt_tokens = tiktoken.count(prompt) + local system_tokens = tiktoken.count(system_prompt) + local selection_tokens = tiktoken.count(selection_message) + local required_tokens = prompt_tokens + system_tokens + selection_tokens + + -- Reserve space for first embedding if its smaller than half of max tokens + local reserved_tokens = 0 + if #embeddings_message.files > 0 then + local file_tokens = tiktoken.count(embeddings_message.files[1]) + if file_tokens < max_tokens / 2 then + reserved_tokens = tiktoken.count(embeddings_message.header) + file_tokens + end + end - -- Calculate how many tokens we can use for history - local history_limit = max_tokens - required_tokens - reserved_tokens - local history_tokens = count_history_tokens(self.history) + -- Calculate how many tokens we can use for history + local history_limit = max_tokens - required_tokens - reserved_tokens + local history_tokens = count_history_tokens(self.history) - -- If we're over history limit, truncate history from the beginning - while history_tokens > history_limit and #self.history > 0 do - local removed = table.remove(self.history, 1) - history_tokens = history_tokens - tiktoken.count(removed.content) - end + -- If we're over history limit, truncate history from the beginning + while history_tokens > history_limit and #self.history > 0 do + local removed = table.remove(self.history, 1) + history_tokens = history_tokens - tiktoken.count(removed.content) + end - -- Now add as many files as possible with remaining token budget - local remaining_tokens = max_tokens - required_tokens - history_tokens - if #embeddings_message.files > 0 then - remaining_tokens = remaining_tokens - tiktoken.count(embeddings_message.header) - local filtered_files = {} - for _, file in ipairs(embeddings_message.files) do - local file_tokens = tiktoken.count(file) - if remaining_tokens - file_tokens >= 0 then - remaining_tokens = remaining_tokens - file_tokens - table.insert(filtered_files, file) - else - break - end - end - embeddings_message.files = filtered_files - end + -- Now add as many files as possible with remaining token budget + local remaining_tokens = max_tokens - required_tokens - history_tokens + if #embeddings_message.files > 0 then + remaining_tokens = remaining_tokens - tiktoken.count(embeddings_message.header) + local filtered_files = {} + for _, file in ipairs(embeddings_message.files) do + local file_tokens = tiktoken.count(file) + if remaining_tokens - file_tokens >= 0 then + remaining_tokens = remaining_tokens - file_tokens + table.insert(filtered_files, file) + else + break + end + end + embeddings_message.files = filtered_files + end - -- Generate the request - local url = 'https://api.githubcopilot.com/chat/completions' - local body = vim.json.encode( - generate_ask_request( - self.history, - prompt, - embeddings_message, - selection_message, - system_prompt, - model, - temperature - ) - ) + local last_message = nil + local errored = false + local full_response = '' - local errored = false - local last_message = nil - local full_response = '' - - local function handle_error(error_msg) - if not errored then - errored = true - log.error(error_msg) - if self.current_job and on_error then - on_error(error_msg) - end - end - end + local function handle_error(err) + errored = true + full_response = err + end - local function callback_func(response) - if not response then - handle_error('Failed to get response') - return - end - - if response.status ~= 200 then - handle_error( - 'Failed to get response: ' .. tostring(response.status) .. '\n' .. response.body - ) - return - end - - log.trace('Full response: ' .. full_response) - log.debug('Last message: ' .. vim.inspect(last_message)) - - if on_done then - on_done( - full_response, - last_message and last_message.usage and last_message.usage.total_tokens, - max_tokens - ) - end - - table.insert(self.history, { - content = prompt, - role = 'user', - }) - - table.insert(self.history, { - content = full_response, - role = 'assistant', - }) - end + local function stream_func(err, line) + if not line or errored then + return + end - local function stream_func(err, line) - if not line or errored or not self.current_job then - return - end - - if err or vim.startswith(line, '{"error"') then - handle_error('Failed to get response: ' .. (err and vim.inspect(err) or line)) - return - end - - line = line:gsub('^%s*data: ', '') - if line == '' or line == '[DONE]' then - return - end - - local ok, content = pcall(vim.json.decode, line, { - luanil = { - object = true, - array = true, - }, - }) - - if not ok then - handle_error('Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line) - return - end - - if not content.choices or #content.choices == 0 then - return - end - - last_message = content - local choice = content.choices[1] - local is_full = choice.message ~= nil - content = is_full and choice.message.content or choice.delta.content - - if not content then - return - end - - if on_progress then - on_progress(content) - end - - full_response = full_response .. content - end + if not self.is_running then + handle_error('') + return + end + + if err or vim.startswith(line, '{"error"') then + handle_error('Failed to get response: ' .. (err and vim.inspect(err) or line)) + return + end + + line = line:gsub('^%s*data: ', '') + if line == '' or line == '[DONE]' then + return + end + + local ok, content = pcall(vim.json.decode, line, { + luanil = { + object = true, + array = true, + }, + }) + + if not ok then + handle_error('Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line) + return + end + + if not content.choices or #content.choices == 0 then + return + end + + last_message = content + local choice = content.choices[1] + local is_full = choice.message ~= nil + content = is_full and choice.message.content or choice.delta.content + + if not content then + return + end + + if on_progress then + on_progress(content) + end + + full_response = full_response .. content + end + + local body = vim.json.encode( + generate_ask_request( + self.history, + prompt, + embeddings_message, + selection_message, + system_prompt, + model, + temperature, + not vim.startswith(model, 'o1') + ) + ) + + if vim.startswith(model, 'claude') then + self:enable_claude(model) + end + + local response = curl_post('https://api.githubcopilot.com/chat/completions', { + timeout = timeout, + headers = self:authenticate(), + body = temp_file(body), + proxy = self.proxy, + insecure = self.allow_insecure, + stream = stream_func, + }) + + if not response then + error('Failed to get response') + return + end + + if response.status ~= 200 then + error('Failed to get response: ' .. tostring(response.status) .. '\n' .. response.body) + return + end + + log.trace('Full response: ' .. full_response) + log.debug('Last message: ' .. vim.inspect(last_message)) + + table.insert(self.history, { + content = prompt, + role = 'user', + }) + + table.insert(self.history, { + content = full_response, + role = 'assistant', + }) - self:with_claude(model, function() - self.current_job = curl - .post(url, { - timeout = timeout, - headers = generate_headers(self.token.token, self.sessionid, self.machineid), - body = temp_file(body), - proxy = self.proxy, - insecure = self.allow_insecure, - stream = stream_func, - callback = callback_func, - on_error = function(err) - err = 'Failed to get response: ' .. vim.inspect(err) - log.error(err) - if self.current_job and on_error then - on_error(err) - end - end, - }) - :after(function() - self.current_job = nil - end) - end, on_error) - end) - end, on_error) - end, on_error) + self.is_running = false + + return full_response, + last_message and last_message.usage and last_message.usage.total_tokens, + max_tokens end --- List available models ----@param callback fun(table):nil -function Copilot:list_models(callback) - self:with_auth(function() - self:with_models(function() - -- Group models by version and shortest ID - local version_map = {} - for id, model in pairs(self.models) do - local version = model.version - if not version_map[version] or #id < #version_map[version] then - version_map[version] = id - end - end +function Copilot:list_models() + local models = self:fetch_models() + + -- Group models by version and shortest ID + local version_map = {} + for id, model in pairs(models) do + local version = model.version + if not version_map[version] or #id < #version_map[version] then + version_map[version] = id + end + end - -- Map to IDs and sort - local result = vim.tbl_values(version_map) - table.sort(result) - callback(result) - end) - end) + -- Map to IDs and sort + local result = vim.tbl_values(version_map) + table.sort(result) + + return result end --- Generate embeddings for the given inputs @@ -725,98 +636,61 @@ function Copilot:embed(inputs, opts) opts = opts or {} local model = opts.model or 'copilot-text-embedding-ada-002' local chunk_size = opts.chunk_size or 15 - local on_done = opts.on_done - local on_error = opts.on_error if not inputs or #inputs == 0 then - if on_done then - on_done({}) - end - return + return {} end - local url = 'https://api.githubcopilot.com/embeddings' - local chunks = {} for i = 1, #inputs, chunk_size do table.insert(chunks, vim.list_slice(inputs, i, i + chunk_size - 1)) end - local jobs = {} + local out = {} + for _, chunk in ipairs(chunks) do - local body = vim.json.encode(generate_embedding_request(chunk, model)) - - table.insert(jobs, function(resolve) - local headers = generate_headers(self.token.token, self.sessionid, self.machineid) - curl.post(url, { - timeout = timeout, - headers = headers, - body = temp_file(body), - proxy = self.proxy, - insecure = self.allow_insecure, - on_error = function(err) - err = 'Failed to get response: ' .. vim.inspect(err) - log.error(err) - resolve() - end, - callback = function(response) - if not response then - resolve() - return - end - - if response.status ~= 200 then - local err = 'Failed to get response: ' .. vim.inspect(response) - log.error(err) - resolve() - return - end - - local ok, content = pcall(vim.json.decode, response.body, { - luanil = { - object = true, - array = true, - }, - }) - - if not ok then - local err = vim.inspect(content) - log.error('Failed to parse response: ' .. err .. '\n' .. response.body) - resolve() - return - end - - resolve(content.data) - end, - }) - end) - end + local response = curl_post('https://api.githubcopilot.com/embeddings', { + timeout = timeout, + headers = self:authenticate(), + body = temp_file(vim.json.encode(generate_embedding_request(chunk, model))), + proxy = self.proxy, + insecure = self.allow_insecure, + }) - self:with_auth(function() - join(function(results) - local out = {} - for chunk_i, chunk_result in ipairs(results) do - if chunk_result then - for _, embedding in ipairs(chunk_result) do - local input = chunks[chunk_i][embedding.index + 1] - table.insert(out, vim.tbl_extend('keep', input, embedding)) - end - end - end + if not response then + error('Failed to get response') + return + end - if on_done then - on_done(out) - end - end, jobs) - end, on_error) + if response.status ~= 200 then + error('Failed to get response: ' .. tostring(response.status) .. '\n' .. response.body) + return + end + + local ok, content = pcall(vim.json.decode, response.body, { + luanil = { + object = true, + array = true, + }, + }) + + if not ok then + error('Failed to parse response: ' .. vim.inspect(content) .. '\n' .. response.body) + return + end + + for _, embedding in ipairs(content.data) do + table.insert(out, vim.tbl_extend('keep', chunk[embedding.index + 1], embedding)) + end + end + + return out end --- Stop the running job function Copilot:stop() - if self.current_job then - local job = self.current_job - self.current_job = nil - job:shutdown() + if self.is_running then + self.is_running = false return true end @@ -876,7 +750,7 @@ end --- Check if there is a running job ---@return boolean function Copilot:running() - return self.current_job ~= nil + return self.is_running end return Copilot diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7f901fea..c085a566 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1,4 +1,5 @@ local default_config = require('CopilotChat.config') +local async = require('plenary.async') local log = require('plenary.log') local Copilot = require('CopilotChat.copilot') local Chat = require('CopilotChat.chat') @@ -350,7 +351,9 @@ end --- Select a Copilot GPT model. function M.select_model() - state.copilot:list_models(function(models) + async.run(function() + local models = state.copilot:list_models() + vim.schedule(function() vim.ui.select(models, { prompt = 'Select a model', @@ -383,7 +386,24 @@ function M.ask(prompt, config, source) M.stop(true, config) end - state.last_system_prompt = system_prompt + local function get_error_message(err) + if type(err) == 'string' then + local message = err:match(':.+:(.+)') or err + message = message:match('^%s*(.-)%s*$') + return message + end + return tostring(err) + end + + local function on_error(err) + vim.schedule(function() + append('\n\n' .. config.error_header .. config.separator .. '\n\n', config) + append('```\n' .. get_error_message(err) .. '\n```', config) + append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) + state.chat:finish() + end) + end + local selection = get_selection() local filetype = selection.filetype or (vim.api.nvim_buf_is_valid(state.source.bufnr) and vim.bo[state.source.bufnr].filetype) @@ -392,6 +412,19 @@ function M.ask(prompt, config, source) vim.api.nvim_buf_is_valid(state.source.bufnr) and vim.api.nvim_buf_get_name(state.source.bufnr) ) + + if not filetype then + on_error('No filetype found for the current buffer') + return + end + + if not filename then + on_error('No filename found for the current buffer') + return + end + + state.last_system_prompt = system_prompt + if selection.prompt_extra then updated_prompt = updated_prompt .. ' ' .. selection.prompt_extra end @@ -411,25 +444,23 @@ function M.ask(prompt, config, source) end updated_prompt = string.gsub(updated_prompt, '@buffers?%s*', '') - local function on_error(err) - vim.schedule(function() - append('\n\n' .. config.error_header .. config.separator .. '\n\n', config) - append('```\n' .. err .. '\n```', config) - append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) - state.chat:finish() - end) - end + async.run(function() + local query_ok, embeddings = pcall(context.find_for_query, state.copilot, { + context = selected_context, + prompt = updated_prompt, + selection = selection.lines, + filename = filename, + filetype = filetype, + bufnr = state.source.bufnr, + }) + + if not query_ok then + on_error(embeddings) + return + end - context.find_for_query(state.copilot, { - context = selected_context, - prompt = updated_prompt, - selection = selection.lines, - filename = filename, - filetype = filetype, - bufnr = state.source.bufnr, - on_error = on_error, - on_done = function(embeddings) - state.copilot:ask(updated_prompt, { + local ask_ok, response, token_count, token_max_count = + pcall(state.copilot.ask, state.copilot, updated_prompt, { selection = selection.lines, embeddings = embeddings, filename = filename, @@ -439,29 +470,32 @@ function M.ask(prompt, config, source) system_prompt = system_prompt, model = config.model, temperature = config.temperature, - on_error = on_error, - on_done = function(response, token_count, token_max_count) - vim.schedule(function() - append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) - state.response = response - if token_count and token_max_count and token_count > 0 then - state.chat:finish(token_count .. '/' .. token_max_count .. ' tokens used') - else - state.chat:finish() - end - if config.callback then - config.callback(response, state.source) - end - end) - end, on_progress = function(token) vim.schedule(function() append(token, config) end) end, }) - end, - }) + + if not ask_ok then + on_error(response) + return + end + + state.response = response + + vim.schedule(function() + append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) + if token_count and token_max_count and token_count > 0 then + state.chat:finish(token_count .. '/' .. token_max_count .. ' tokens used') + else + state.chat:finish() + end + if config.callback then + config.callback(response, state.source) + end + end) + end) end --- Stop current copilot output and optionally reset the chat ten show the help message. diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 7ebd13f5..e488455f 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -74,10 +74,6 @@ function M.load(tokenizer, on_done) end) end -function M.available() - return tiktoken_core ~= nil -end - function M.encode(prompt) if not tiktoken_core then return nil diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index cf4454d9..abc07126 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -42,26 +42,6 @@ function M.is_stable() return vim.fn.has('nvim-0.10.0') == 0 end ---- Join multiple async functions ----@param on_done function The function to call when all the async functions are done ----@param fns table The async functions -function M.join(on_done, fns) - local count = #fns - local results = {} - local function done() - count = count - 1 - if count == 0 then - on_done(results) - end - end - for i, fn in ipairs(fns) do - fn(function(result) - results[i] = result - done() - end) - end -end - --- Writes text to a temporary file and returns path ---@param text string The text to write ---@return string? From 39c1a1526be521927fcbb82447f1910b6b7480a7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Nov 2024 17:17:48 +0100 Subject: [PATCH 102/338] Fix tests failing on plenary.async Signed-off-by: Tomas Slusny --- test/plugin_spec.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/test/plugin_spec.lua b/test/plugin_spec.lua index 686b3d05..145c3fa4 100644 --- a/test/plugin_spec.lua +++ b/test/plugin_spec.lua @@ -1,4 +1,5 @@ -- Mock packages +package.loaded['plenary.async'] = {} package.loaded['plenary.curl'] = {} package.loaded['plenary.log'] = {} From 2048c22f8eaa662bfacc02a4b4a2e5152354f265 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Nov 2024 17:20:41 +0100 Subject: [PATCH 103/338] Fix missing plenary.wrap in tests Signed-off-by: Tomas Slusny --- test/plugin_spec.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/plugin_spec.lua b/test/plugin_spec.lua index 145c3fa4..cb3e4b47 100644 --- a/test/plugin_spec.lua +++ b/test/plugin_spec.lua @@ -1,5 +1,11 @@ -- Mock packages -package.loaded['plenary.async'] = {} +package.loaded['plenary.async'] = { + wrap = function(fn) + return function(...) + return fn(...) + end + end, +} package.loaded['plenary.curl'] = {} package.loaded['plenary.log'] = {} From 1e7141e914cc26a3d191f869e52719b902138f6b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Nov 2024 17:16:12 +0100 Subject: [PATCH 104/338] Add config for setting log level Closes #461 Signed-off-by: Tomas Slusny --- README.md | 15 ++++++++------- lua/CopilotChat/config.lua | 4 +++- lua/CopilotChat/init.lua | 18 ++++++++++++------ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9291d95c..1f1be5f6 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,7 @@ return { }, build = "make tiktoken", -- Only on MacOS or Linux opts = { - debug = true, -- Enable debugging - -- See Configuration section for rest + -- See Configuration section for options }, -- See Commands section for default commands if you want to lazy load on them }, @@ -64,8 +63,7 @@ call plug#end() lua << EOF require("CopilotChat").setup { - debug = true, -- Enable debugging - -- See Configuration section for rest + -- See Configuration section for options } EOF ``` @@ -88,8 +86,7 @@ git clone -b canary https://github.com/CopilotC-Nvim/CopilotChat.nvim ```lua require("CopilotChat").setup { - debug = true, -- Enable debugging - -- See Configuration section for rest + -- See Configuration section for options } ``` @@ -191,6 +188,9 @@ actions.pick(actions.help_actions()) actions.pick(actions.prompt_actions({ selection = require("CopilotChat.select").visual, })) + +-- Programatically set log level +chat.log_level("debug") ``` ## Configuration @@ -201,7 +201,8 @@ Also see [here](/lua/CopilotChat/config.lua): ```lua { - debug = false, -- Enable debug logging + debug = false, -- Enable debug logging (same as 'log_level = 'debug') + log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 556f1e61..a6e1ac02 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -56,6 +56,7 @@ local select = require('CopilotChat.select') --- CopilotChat default configuration ---@class CopilotChat.config ---@field debug boolean? +---@field log_level string? ---@field proxy string? ---@field allow_insecure boolean? ---@field system_prompt string? @@ -79,7 +80,8 @@ local select = require('CopilotChat.select') ---@field window CopilotChat.config.window? ---@field mappings CopilotChat.config.mappings? return { - debug = false, -- Enable debug logging + debug = false, -- Enable debug logging (same as 'log_level = 'debug') + log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index c085a566..0ff35c49 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -586,14 +586,15 @@ function M.load(name, history_path) M.open() end ---- Enables/disables debug ----@param debug boolean -function M.debug(debug) - M.config.debug = debug +--- Set the log level +---@param level string +function M.log_level(level) + M.config.log_level = level + M.config.debug = level == 'debug' local logfile = string.format('%s/%s.log', vim.fn.stdpath('state'), plugin_name) log.new({ plugin = plugin_name, - level = debug and 'debug' or 'info', + level = level, outfile = logfile, }, true) log.logfile = logfile @@ -643,7 +644,12 @@ function M.setup(config) end state.copilot = Copilot(M.config.proxy, M.config.allow_insecure) - M.debug(M.config.debug) + + if M.config.debug then + M.log_level('debug') + else + M.log_level(M.config.log_level) + end local hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights') vim.api.nvim_set_hl(hl_ns, '@diff.plus', { bg = blend_color_with_neovim_bg('DiffAdd', 20) }) From 8d9097880dd86c8f698ba3d1cf9e1f66ca8283f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Nov 2024 17:53:08 +0000 Subject: [PATCH 105/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index a56b49cd..f4c86f3f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -60,8 +60,7 @@ LAZY.NVIM ~ }, build = "make tiktoken", -- Only on MacOS or Linux opts = { - debug = true, -- Enable debugging - -- See Configuration section for rest + -- See Configuration section for options }, -- See Commands section for default commands if you want to lazy load on them }, @@ -85,8 +84,7 @@ Similar to the lazy setup, you can use the following configuration: lua << EOF require("CopilotChat").setup { - debug = true, -- Enable debugging - -- See Configuration section for rest + -- See Configuration section for options } EOF < @@ -110,8 +108,7 @@ MANUAL ~ >lua require("CopilotChat").setup { - debug = true, -- Enable debugging - -- See Configuration section for rest + -- See Configuration section for options } < @@ -220,6 +217,9 @@ API ~ actions.pick(actions.prompt_actions({ selection = require("CopilotChat.select").visual, })) + + -- Programatically set log level + chat.log_level("debug") < @@ -232,7 +232,8 @@ Also see here : >lua { - debug = false, -- Enable debug logging + debug = false, -- Enable debug logging (same as 'log_level = 'debug') + log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections From 5bf9c1ba31ad27313812b09782c089e34b51e399 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Nov 2024 19:23:46 +0100 Subject: [PATCH 106/338] Improve job cancellation Generate unique ID for every job run and reset/change it when new job is started or when job is stopped. Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 35 ++++++++++++++++++++++++----------- lua/CopilotChat/init.lua | 4 ++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 4ca2bfba..9fc51b03 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -292,7 +292,7 @@ local Copilot = class(function(self, proxy, allow_insecure) self.machineid = machine_id() self.models = nil self.claude_enabled = false - self.is_running = false + self.current_job = nil end) function Copilot:authenticate() @@ -414,8 +414,6 @@ end ---@param prompt string: The prompt to send to Copilot ---@param opts CopilotChat.copilot.ask.opts: Options for the request function Copilot:ask(prompt, opts) - self.is_running = true - opts = opts or {} local embeddings = opts.embeddings or {} local filename = opts.filename or '' @@ -427,6 +425,8 @@ function Copilot:ask(prompt, opts) local model = opts.model or 'gpt-4o-2024-05-13' local temperature = opts.temperature or 0.1 local on_progress = opts.on_progress + local job_id = uuid() + self.current_job = job_id log.trace('System prompt: ' .. system_prompt) log.trace('Selection: ' .. selection) @@ -506,8 +506,7 @@ function Copilot:ask(prompt, opts) return end - if not self.is_running then - handle_error('') + if self.current_job ~= job_id then return end @@ -567,7 +566,7 @@ function Copilot:ask(prompt, opts) ) if vim.startswith(model, 'claude') then - self:enable_claude(model) + self:enable_claude() end local response = curl_post('https://api.githubcopilot.com/chat/completions', { @@ -579,6 +578,12 @@ function Copilot:ask(prompt, opts) stream = stream_func, }) + if self.current_job ~= job_id then + return nil, nil, nil + end + + self.current_job = nil + if not response then error('Failed to get response') return @@ -589,6 +594,16 @@ function Copilot:ask(prompt, opts) return end + if errored then + error(full_response) + return + end + + if full_response == '' then + error('Failed to get response: empty response') + return + end + log.trace('Full response: ' .. full_response) log.debug('Last message: ' .. vim.inspect(last_message)) @@ -602,8 +617,6 @@ function Copilot:ask(prompt, opts) role = 'assistant', }) - self.is_running = false - return full_response, last_message and last_message.usage and last_message.usage.total_tokens, max_tokens @@ -689,8 +702,8 @@ end --- Stop the running job function Copilot:stop() - if self.is_running then - self.is_running = false + if self.current_job ~= nil then + self.current_job = nil return true end @@ -750,7 +763,7 @@ end --- Check if there is a running job ---@return boolean function Copilot:running() - return self.is_running + return self.current_job ~= nil end return Copilot diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0ff35c49..a47d980c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -482,6 +482,10 @@ function M.ask(prompt, config, source) return end + if not response then + return + end + state.response = response vim.schedule(function() From 301619a347eebc7807dbd28324360af19fe0fb87 Mon Sep 17 00:00:00 2001 From: Toddneal Stallworth <43554061+Moriango@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:06:39 -0800 Subject: [PATCH 107/338] Fix spelling mistakes in API section (#476) * Testing SSH * Fixed spelling mistakes --- README.md | 2 +- doc/CopilotChat.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f1be5f6..630ed54a 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ actions.pick(actions.prompt_actions({ selection = require("CopilotChat.select").visual, })) --- Programatically set log level +-- Programmatically set log level chat.log_level("debug") ``` diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index f4c86f3f..cf623262 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -218,7 +218,7 @@ API ~ selection = require("CopilotChat.select").visual, })) - -- Programatically set log level + -- Programmatically set log level chat.log_level("debug") < From e3d7b4ba6d1fcfe227b2b23be8c1a051059aa595 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 21:14:21 +0100 Subject: [PATCH 108/338] docs: add Moriango as a contributor for doc (#477) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3ccf81a4..0ca4d52d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -270,6 +270,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/300342?v=4", "profile": "https://lisk.in/", "contributions": ["code"] + }, + { + "login": "Moriango", + "name": "Toddneal Stallworth", + "avatar_url": "https://avatars.githubusercontent.com/u/43554061?v=4", + "profile": "https://github.com/Moriango", + "contributions": ["doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 630ed54a..627f63a5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square)](#contributors-) @@ -619,6 +619,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d JakubPecenka
JakubPecenka

💻 thomastthai
thomastthai

📖 Tomáš Janoušek
Tomáš Janoušek

💻 + Toddneal Stallworth
Toddneal Stallworth

📖 From 05931552f35dc5b04f80af87792b8e6b9589247e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 13 Nov 2024 22:29:08 +0100 Subject: [PATCH 109/338] Add handling for no response from stream and improve curl performance Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 116 +++++++++++++++++++++++++++++------- lua/CopilotChat/init.lua | 1 + 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 9fc51b03..fab34da2 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -48,15 +48,30 @@ local version_headers = { .. vim.version().patch, ['editor-plugin-version'] = 'CopilotChat.nvim/2.0.0', ['user-agent'] = 'CopilotChat.nvim/2.0.0', + ['sec-fetch-site'] = 'none', + ['sec-fetch-mode'] = 'no-cors', + ['sec-fetch-dest'] = 'empty', + ['priority'] = 'u=4, i', + -- ['x-github-api-version'] = '2023-07-07', } local curl_get = async.wrap(function(url, opts, callback) - opts = vim.tbl_deep_extend('force', opts, { callback = callback }) + opts = vim.tbl_deep_extend('force', opts, { + callback = callback, + on_error = function(err) + callback(nil, vim.inspect(err)) + end, + }) curl.get(url, opts) end, 3) local curl_post = async.wrap(function(url, opts, callback) - opts = vim.tbl_deep_extend('force', opts, { callback = callback }) + opts = vim.tbl_deep_extend('force', opts, { + callback = callback, + on_error = function(err) + callback(nil, vim.inspect(err)) + end, + }) curl.post(url, opts) end, 3) @@ -79,7 +94,8 @@ local function machine_id() local hex_chars = '0123456789abcdef' local hex = '' for _ = 1, length do - hex = hex .. hex_chars:sub(math.random(1, #hex_chars), math.random(1, #hex_chars)) + local index = math.random(1, #hex_chars) + hex = hex .. hex_chars:sub(index, index) end return hex end @@ -235,7 +251,6 @@ local function generate_ask_request( if stream then return { - intent = true, model = model, n = 1, stream = true, @@ -293,6 +308,26 @@ local Copilot = class(function(self, proxy, allow_insecure) self.models = nil self.claude_enabled = false self.current_job = nil + self.extra_args = { + -- Retry failed requests twice + '--retry', + '2', + -- Wait 1 second between retries + '--retry-delay', + '1', + -- Maximum time for the request + '--max-time', + math.floor(timeout * 2 / 1000), + -- Timeout for initial connection + '--connect-timeout', + '10', + '--no-keepalive', -- Don't reuse connections + '--tcp-nodelay', -- Disable Nagle's algorithm for faster streaming + '--no-buffer', -- Disable output buffering for streaming + '--fail', -- Return error on HTTP errors (4xx, 5xx) + '--silent', -- Don't show progress meter + '--show-error', -- Show errors even when silent + } end) function Copilot:authenticate() @@ -314,13 +349,18 @@ function Copilot:authenticate() headers[key] = value end - local response = curl_get('https://api.github.com/copilot_internal/v2/token', { + local response, err = curl_get('https://api.github.com/copilot_internal/v2/token', { timeout = timeout, headers = headers, proxy = self.proxy, insecure = self.allow_insecure, + raw = self.extra_args, }) + if err then + error(err) + end + if response.status ~= 200 then error('Failed to authenticate: ' .. tostring(response.status)) end @@ -351,13 +391,18 @@ function Copilot:fetch_models() return self.models end - local response = curl_get('https://api.githubcopilot.com/models', { + local response, err = curl_get('https://api.githubcopilot.com/models', { timeout = timeout, headers = self:authenticate(), proxy = self.proxy, insecure = self.allow_insecure, + raw = self.extra_args, }) + if err then + error(err) + end + if response.status ~= 200 then error('Failed to fetch models: ' .. tostring(response.status)) end @@ -385,14 +430,19 @@ function Copilot:enable_claude() local business_msg = 'Claude is probably enabled (for business users needs to be enabled manually).' - local response = curl_post('https://api.githubcopilot.com/models/claude-3.5-sonnet/policy', { + local response, err = curl_post('https://api.githubcopilot.com/models/claude-3.5-sonnet/policy', { timeout = timeout, headers = self:authenticate(), proxy = self.proxy, insecure = self.allow_insecure, body = temp_file('{"state": "enabled"}'), + raw = self.extra_args, }) + if err then + error(err) + end + -- Handle business user case if response.status ~= 200 and string.find(tostring(response.body), business_check) then self.claude_enabled = true @@ -494,29 +544,44 @@ function Copilot:ask(prompt, opts) local last_message = nil local errored = false + local finished = false local full_response = '' - local function handle_error(err) - errored = true - full_response = err + local function finish_stream(err, job) + if err then + errored = true + full_response = err + end + + finished = true + vim.schedule(function() + job:shutdown() + end) end - local function stream_func(err, line) - if not line or errored then + local function stream_func(err, line, job) + if not line or errored or finished then return end if self.current_job ~= job_id then + finish_stream(nil, job) return end if err or vim.startswith(line, '{"error"') then - handle_error('Failed to get response: ' .. (err and vim.inspect(err) or line)) + finish_stream('Failed to get response: ' .. (err and vim.inspect(err) or line), job) + return + end + + if not vim.startswith(line, 'data: ') then return end - line = line:gsub('^%s*data: ', '') - if line == '' or line == '[DONE]' then + line = line:gsub('^%s*data:%s*', ''):gsub('%s*$', '') + + if line == '[DONE]' then + finish_stream(nil, job) return end @@ -528,7 +593,7 @@ function Copilot:ask(prompt, opts) }) if not ok then - handle_error('Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line) + finish_stream('Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line, job) return end @@ -538,8 +603,7 @@ function Copilot:ask(prompt, opts) last_message = content local choice = content.choices[1] - local is_full = choice.message ~= nil - content = is_full and choice.message.content or choice.delta.content + content = choice.message and choice.message.content or choice.delta and choice.delta.content if not content then return @@ -569,13 +633,14 @@ function Copilot:ask(prompt, opts) self:enable_claude() end - local response = curl_post('https://api.githubcopilot.com/chat/completions', { + local response, err = curl_post('https://api.githubcopilot.com/chat/completions', { timeout = timeout, headers = self:authenticate(), body = temp_file(body), proxy = self.proxy, insecure = self.allow_insecure, stream = stream_func, + raw = self.extra_args, }) if self.current_job ~= job_id then @@ -584,6 +649,11 @@ function Copilot:ask(prompt, opts) self.current_job = nil + if err then + error(err) + return + end + if not response then error('Failed to get response') return @@ -662,14 +732,20 @@ function Copilot:embed(inputs, opts) local out = {} for _, chunk in ipairs(chunks) do - local response = curl_post('https://api.githubcopilot.com/embeddings', { + local response, err = curl_post('https://api.githubcopilot.com/embeddings', { timeout = timeout, headers = self:authenticate(), body = temp_file(vim.json.encode(generate_embedding_request(chunk, model))), proxy = self.proxy, insecure = self.allow_insecure, + raw = self.extra_args, }) + if err then + error(err) + return + end + if not response then error('Failed to get response') return diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index a47d980c..c7d4923f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -396,6 +396,7 @@ function M.ask(prompt, config, source) end local function on_error(err) + log.error(err) vim.schedule(function() append('\n\n' .. config.error_header .. config.separator .. '\n\n', config) append('```\n' .. get_error_message(err) .. '\n```', config) From 93b7aaf25a6d17015a1204a6636e366649793ff8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 14 Nov 2024 08:00:09 +0000 Subject: [PATCH 110/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index cf623262..67f2aba3 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 13 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 14 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -598,7 +598,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻thomastthai📖Tomáš Janoušek💻This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻thomastthai📖Tomáš Janoušek💻Toddneal Stallworth📖This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -613,7 +613,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From e9e8322d30fc3b0bdaf05d3bed16c12e1d936551 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 14 Nov 2024 09:21:27 +0100 Subject: [PATCH 111/338] Add max_tokens to chat requests This is to match what VSCode is doing Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index fab34da2..7c028f2e 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -214,6 +214,7 @@ local function generate_ask_request( system_prompt, model, temperature, + max_output_tokens, stream ) local messages = {} @@ -249,22 +250,23 @@ local function generate_ask_request( role = 'user', }) + local out = { + messages = messages, + model = model, + stream = stream, + } + + if max_output_tokens then + out.max_tokens = max_output_tokens + end + if stream then - return { - model = model, - n = 1, - stream = true, - temperature = temperature, - top_p = 1, - messages = messages, - } - else - return { - messages = messages, - stream = false, - model = model, - } + out.n = 1 + out.temperature = temperature + out.top_p = 1 end + + return out end local function generate_embedding_request(inputs, model) @@ -489,8 +491,8 @@ function Copilot:ask(prompt, opts) local models = self:fetch_models() local capabilities = models[model] and models[model].capabilities - or { limits = { max_prompt_tokens = 8192 }, tokenizer = 'cl100k_base' } local max_tokens = capabilities.limits.max_prompt_tokens -- FIXME: Is max_prompt_tokens the right limit? + local max_output_tokens = capabilities.limits.max_output_tokens local tokenizer = capabilities.tokenizer log.debug('Max tokens: ' .. max_tokens) log.debug('Tokenizer: ' .. tokenizer) @@ -625,6 +627,7 @@ function Copilot:ask(prompt, opts) system_prompt, model, temperature, + max_output_tokens, not vim.startswith(model, 'o1') ) ) From 0d4e1085ef16b3ed7937a1fffef87ec6bd3cccba Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 14 Nov 2024 18:10:43 +0100 Subject: [PATCH 112/338] Deduplicate curl requests Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 127 +++++++++++++++++------------------- 1 file changed, 61 insertions(+), 66 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 7c028f2e..9fcbdebf 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -300,8 +300,6 @@ local function count_history_tokens(history) end local Copilot = class(function(self, proxy, allow_insecure) - self.proxy = proxy - self.allow_insecure = allow_insecure self.github_token = get_cached_token() self.history = {} self.token = nil @@ -310,25 +308,30 @@ local Copilot = class(function(self, proxy, allow_insecure) self.models = nil self.claude_enabled = false self.current_job = nil - self.extra_args = { - -- Retry failed requests twice - '--retry', - '2', - -- Wait 1 second between retries - '--retry-delay', - '1', - -- Maximum time for the request - '--max-time', - math.floor(timeout * 2 / 1000), - -- Timeout for initial connection - '--connect-timeout', - '10', - '--no-keepalive', -- Don't reuse connections - '--tcp-nodelay', -- Disable Nagle's algorithm for faster streaming - '--no-buffer', -- Disable output buffering for streaming - '--fail', -- Return error on HTTP errors (4xx, 5xx) - '--silent', -- Don't show progress meter - '--show-error', -- Show errors even when silent + self.request_args = { + timeout = timeout, + proxy = proxy, + insecure = allow_insecure, + raw = { + -- Retry failed requests twice + '--retry', + '2', + -- Wait 1 second between retries + '--retry-delay', + '1', + -- Maximum time for the request + '--max-time', + math.floor(timeout * 2 / 1000), + -- Timeout for initial connection + '--connect-timeout', + '10', + '--no-keepalive', -- Don't reuse connections + '--tcp-nodelay', -- Disable Nagle's algorithm for faster streaming + '--no-buffer', -- Disable output buffering for streaming + '--fail', -- Return error on HTTP errors (4xx, 5xx) + '--silent', -- Don't show progress meter + '--show-error', -- Show errors even when silent + }, } end) @@ -351,13 +354,12 @@ function Copilot:authenticate() headers[key] = value end - local response, err = curl_get('https://api.github.com/copilot_internal/v2/token', { - timeout = timeout, - headers = headers, - proxy = self.proxy, - insecure = self.allow_insecure, - raw = self.extra_args, - }) + local response, err = curl_get( + 'https://api.github.com/copilot_internal/v2/token', + vim.tbl_extend('force', self.request_args, { + headers = headers, + }) + ) if err then error(err) @@ -393,13 +395,12 @@ function Copilot:fetch_models() return self.models end - local response, err = curl_get('https://api.githubcopilot.com/models', { - timeout = timeout, - headers = self:authenticate(), - proxy = self.proxy, - insecure = self.allow_insecure, - raw = self.extra_args, - }) + local response, err = curl_get( + 'https://api.githubcopilot.com/models', + vim.tbl_extend('force', self.request_args, { + headers = self:authenticate(), + }) + ) if err then error(err) @@ -432,14 +433,13 @@ function Copilot:enable_claude() local business_msg = 'Claude is probably enabled (for business users needs to be enabled manually).' - local response, err = curl_post('https://api.githubcopilot.com/models/claude-3.5-sonnet/policy', { - timeout = timeout, - headers = self:authenticate(), - proxy = self.proxy, - insecure = self.allow_insecure, - body = temp_file('{"state": "enabled"}'), - raw = self.extra_args, - }) + local response, err = curl_post( + 'https://api.githubcopilot.com/models/claude-3.5-sonnet/policy', + vim.tbl_extend('force', self.request_args, { + headers = self:authenticate(), + body = vim.json.encode({ state = 'enabled' }), + }) + ) if err then error(err) @@ -636,15 +636,14 @@ function Copilot:ask(prompt, opts) self:enable_claude() end - local response, err = curl_post('https://api.githubcopilot.com/chat/completions', { - timeout = timeout, - headers = self:authenticate(), - body = temp_file(body), - proxy = self.proxy, - insecure = self.allow_insecure, - stream = stream_func, - raw = self.extra_args, - }) + local response, err = curl_post( + 'https://api.githubcopilot.com/chat/completions', + vim.tbl_extend('force', self.request_args, { + headers = self:authenticate(), + body = temp_file(body), + stream = stream_func, + }) + ) if self.current_job ~= job_id then return nil, nil, nil @@ -727,22 +726,18 @@ function Copilot:embed(inputs, opts) return {} end - local chunks = {} - for i = 1, #inputs, chunk_size do - table.insert(chunks, vim.list_slice(inputs, i, i + chunk_size - 1)) - end - local out = {} - for _, chunk in ipairs(chunks) do - local response, err = curl_post('https://api.githubcopilot.com/embeddings', { - timeout = timeout, - headers = self:authenticate(), - body = temp_file(vim.json.encode(generate_embedding_request(chunk, model))), - proxy = self.proxy, - insecure = self.allow_insecure, - raw = self.extra_args, - }) + for i = 1, #inputs, chunk_size do + local chunk = vim.list_slice(inputs, i, i + chunk_size - 1) + local body = vim.json.encode(generate_embedding_request(chunk, model)) + local response, err = curl_post( + 'https://api.githubcopilot.com/embeddings', + vim.tbl_extend('force', self.request_args, { + headers = self:authenticate(), + body = temp_file(body), + }) + ) if err then error(err) From e2fa8146787085d40d5736d40a089a9326b81f5b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 14 Nov 2024 20:08:03 +0100 Subject: [PATCH 113/338] Improve help display Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index c7d4923f..b5397899 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -217,21 +217,26 @@ end --- Get the info for a key. ---@param name string ---@param key CopilotChat.config.mapping? +---@param surround string|nil ---@return string -local function key_to_info(name, key) +local function key_to_info(name, key, surround) if not key then return '' end + if not surround then + surround = '' + end + local out = '' if key.normal and key.normal ~= '' then - out = out .. "'" .. key.normal .. "' in normal mode" + out = out .. surround .. key.normal .. surround end - if key.insert and key.insert ~= '' then + if key.insert and key.insert ~= '' and key.insert ~= key.normal then if out ~= '' then out = out .. ' or ' end - out = out .. "'" .. key.insert .. "' in insert mode" + out = out .. surround .. key.insert .. surround .. ' in insert mode' end if out == '' then @@ -759,8 +764,13 @@ function M.setup(config) return a < b end) for _, name in ipairs(chat_keys) do - local key = M.config.mappings[name] - chat_help = chat_help .. key_to_info(name, key) .. '\n' + if name ~= 'close' then + local key = M.config.mappings[name] + local info = key_to_info(name, key, '`') + if info ~= '' then + chat_help = chat_help .. info .. '\n' + end + end end chat_help = chat_help .. M.config.separator .. '\n' state.help:show(chat_help, 'markdown', 'markdown', state.chat.winnr) From 9e41a6c3525d8e47d61601e0fd98fcbc091e487a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 14 Nov 2024 20:50:42 +0100 Subject: [PATCH 114/338] Automatically clear copilot diagnostics on asking new question Closes #362 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 7 +++++-- lua/CopilotChat/init.lua | 2 ++ lua/CopilotChat/prompts.lua | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index a6e1ac02..e8993d9e 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -119,7 +119,6 @@ return { Review = { prompt = '/COPILOT_REVIEW Review the selected code.', callback = function(response, source) - local ns = vim.api.nvim_create_namespace('copilot_review') local diagnostics = {} for line in response:gmatch('[^\r\n]+') do if line:find('^line=') then @@ -152,7 +151,11 @@ return { end end end - vim.diagnostic.set(ns, source.bufnr, diagnostics) + vim.diagnostic.set( + vim.api.nvim_create_namespace('copilot_diagnostics'), + source.bufnr, + diagnostics + ) end, }, Fix = { diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index b5397899..4e1a1151 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -387,6 +387,8 @@ function M.ask(prompt, config, source) M.open(config, source) + vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot_diagnostics')) + if config.clear_chat_on_new_prompt then M.stop(true, config) end diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index d18a4ada..e12b0ff1 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -90,6 +90,8 @@ line=-: If you find multiple issues on the same line, list each issue separately within the same feedback statement, using a semicolon to separate them. +At the end of your review, add this: "**`To clear buffer highlights, please ask a different question.`**". + Example feedback: line=3: The variable name 'x' is unclear. Comment next to variable declaration is unnecessary. line=8: Expression is overly complex. Break down the expression into simpler components. From 83bb411b576c71d46fc24bd0a1fae31a0d562417 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 14 Nov 2024 21:21:08 +0100 Subject: [PATCH 115/338] Fix error matching Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 4e1a1151..e7f07c85 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -395,7 +395,9 @@ function M.ask(prompt, config, source) local function get_error_message(err) if type(err) == 'string' then - local message = err:match(':.+:(.+)') or err + -- Match first occurrence of :something: and capture rest + local message = err:match('^[^:]+:[^:]+:(.+)') or err + -- Trim whitespace message = message:match('^%s*(.-)%s*$') return message end From a18542475c00d54744acd6728fe44439c80fbfe0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 14 Nov 2024 21:53:43 +0100 Subject: [PATCH 116/338] Add separators when showing diff/prompt/selection windows Makes the display a bit nicer Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index e7f07c85..5adf9efc 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -863,6 +863,7 @@ function M.setup(config) ctxlen = #selection.lines, })) + diff = diff .. '\n' .. M.config.separator .. '\n' state.diff:show(diff, filetype, 'diff', state.chat.winnr) end end) @@ -873,6 +874,7 @@ function M.setup(config) return end + prompt = prompt .. '\n' .. M.config.separator .. '\n' state.system_prompt:show(prompt, 'markdown', 'markdown', state.chat.winnr) end) @@ -884,9 +886,12 @@ function M.setup(config) local filetype = selection.filetype or vim.bo[state.source.bufnr].filetype local lines = selection.lines - if vim.trim(lines) ~= '' then - state.user_selection:show(lines, filetype, filetype, state.chat.winnr) + if vim.trim(lines) == '' then + return end + + lines = lines .. '\n' .. M.config.separator .. '\n' + state.user_selection:show(lines, filetype, filetype, state.chat.winnr) end) vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { From 2c9a1732e19538a98942906e5470d11794234970 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Nov 2024 01:00:08 +0100 Subject: [PATCH 117/338] Load github token asynchronously This prevents initial hiccup when opening first window of a chat session Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 11 +++++++---- lua/CopilotChat/overlay.lua | 2 +- lua/CopilotChat/spinner.lua | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 9fcbdebf..cf0fd68d 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -300,8 +300,8 @@ local function count_history_tokens(history) end local Copilot = class(function(self, proxy, allow_insecure) - self.github_token = get_cached_token() self.history = {} + self.github_token = nil self.token = nil self.sessionid = nil self.machineid = machine_id() @@ -337,9 +337,12 @@ end) function Copilot:authenticate() if not self.github_token then - error( - 'No GitHub token found, please use `:Copilot auth` to set it up from copilot.lua or `:Copilot setup` for copilot.vim' - ) + self.github_token = get_cached_token() + if not self.github_token then + error( + 'No GitHub token found, please use `:Copilot auth` to set it up from copilot.lua or `:Copilot setup` for copilot.vim' + ) + end end if diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index 1c12b442..94d86021 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -87,7 +87,7 @@ function Overlay:show_help(msg, offset) local help_ns = vim.api.nvim_create_namespace('copilot-chat-help') local line = vim.api.nvim_buf_line_count(self.bufnr) + offset vim.api.nvim_buf_set_extmark(self.bufnr, help_ns, math.max(0, line - 1), 0, { - id = help_ns, + id = 1, hl_mode = 'combine', priority = 100, virt_lines = vim.tbl_map(function(t) diff --git a/lua/CopilotChat/spinner.lua b/lua/CopilotChat/spinner.lua index da85bc75..4c3f8f13 100644 --- a/lua/CopilotChat/spinner.lua +++ b/lua/CopilotChat/spinner.lua @@ -52,7 +52,7 @@ function Spinner:start() math.max(0, vim.api.nvim_buf_line_count(self.bufnr) - 1), 0, { - id = self.ns, + id = 1, hl_mode = 'combine', priority = 100, virt_text = vim.tbl_map(function(t) @@ -77,7 +77,7 @@ function Spinner:finish() timer:stop() timer:close() - vim.api.nvim_buf_del_extmark(self.bufnr, self.ns, self.ns) + vim.api.nvim_buf_del_extmark(self.bufnr, self.ns, 1) end return Spinner From 85a1679837d2698273e98295a6ff4ba66477ea1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 15 Nov 2024 00:01:44 +0000 Subject: [PATCH 118/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 67f2aba3..3d4cc673 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 14 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 15 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 78ea7304b2ae851c09b61a3f694b13f171348970 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Nov 2024 08:31:22 +0100 Subject: [PATCH 119/338] Send code 0 in shutdown to propagate error message properly Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index cf0fd68d..7db68afa 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -559,9 +559,7 @@ function Copilot:ask(prompt, opts) end finished = true - vim.schedule(function() - job:shutdown() - end) + job:shutdown(0) end local function stream_func(err, line, job) From 8bc31066f72a8c55630980f7d008b88425d6b33b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Nov 2024 10:40:58 +0100 Subject: [PATCH 120/338] Revamp how diagnostics are handled - Automatically include diagnostics in selections. This means that the chat will always know about them as well - With diagnostics automatically included, CopilotChatFixDiagnostics is no longer necessary, CopilotChatFix is enough - With diagnostics automatically included, actions.help_actions also no longer serves any purpose, CopilotChatExplain + CopilotChatFix is enough - To better incorporate new workflow, also default to visual || buffer for selection (which was popular config adjustment in first place anyway) Signed-off-by: Tomas Slusny --- README.md | 34 ++---------- lua/CopilotChat/actions.lua | 31 ++--------- lua/CopilotChat/config.lua | 20 ++++--- lua/CopilotChat/copilot.lua | 57 +++++++++++++++----- lua/CopilotChat/init.lua | 62 ++++++++------------- lua/CopilotChat/select.lua | 105 ++++++++++++++++++------------------ lua/CopilotChat/utils.lua | 4 ++ 7 files changed, 143 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 627f63a5..6157151a 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,12 @@ Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabl #### Commands coming from default prompts -- `:CopilotChatExplain` - Write an explanation for the active selection as paragraphs of text +- `:CopilotChatExplain` - Write an explanation for the active selection and diagnostics as paragraphs of text - `:CopilotChatReview` - Review the selected code - `:CopilotChatFix` - There is a problem in this code. Rewrite the code to show it with the bug fixed - `:CopilotChatOptimize` - Optimize the selected code to improve performance and readability - `:CopilotChatDocs` - Please add documentation comment for the selection - `:CopilotChatTests` - Please generate tests for my code -- `:CopilotChatFixDiagnostic` - Please assist with the following diagnostic issue in file - `:CopilotChatCommit` - Write commit message for the change with commitizen convention - `:CopilotChatCommitStaged` - Write commit message for the change with commitizen convention @@ -181,9 +180,6 @@ local response = chat.response() -- Pick a prompt using vim.ui.select local actions = require("CopilotChat.actions") --- Pick help actions -actions.pick(actions.help_actions()) - -- Pick prompt actions actions.pick(actions.prompt_actions({ selection = require("CopilotChat.select").visual, @@ -227,15 +223,15 @@ Also see [here](/lua/CopilotChat/config.lua): history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received - -- default selection (visual or line) + -- default selection selection = function(source) - return select.visual(source) or select.line(source) + return select.visual(source) or select.buffer(source) end, -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection as paragraphs of text.', + prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection and diagnostics as paragraphs of text.', }, Review = { prompt = '/COPILOT_REVIEW Review the selected code.', @@ -255,10 +251,6 @@ Also see [here](/lua/CopilotChat/config.lua): Tests = { prompt = '/COPILOT_GENERATE Please generate tests for my code.', }, - FixDiagnostic = { - prompt = 'Please assist with the following diagnostic issue in file:', - selection = select.diagnostics, - }, Commit = { prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', selection = select.gitdiff, @@ -462,15 +454,6 @@ Requires [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) plug ```lua -- lazy.nvim keys - -- Show help actions with telescope - { - "cch", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.telescope").pick(actions.help_actions()) - end, - desc = "CopilotChat - Help actions", - }, -- Show prompts actions with telescope { "ccp", @@ -494,15 +477,6 @@ Requires [fzf-lua](https://github.com/ibhagwan/fzf-lua) plugin to be installed. ```lua -- lazy.nvim keys - -- Show help actions with fzf-lua - { - "cch", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.fzflua").pick(actions.help_actions()) - end, - desc = "CopilotChat - Help actions", - }, -- Show prompts actions with fzf-lua { "ccp", diff --git a/lua/CopilotChat/actions.lua b/lua/CopilotChat/actions.lua index 1d838a7b..4ae5dfeb 100644 --- a/lua/CopilotChat/actions.lua +++ b/lua/CopilotChat/actions.lua @@ -2,37 +2,14 @@ ---@field prompt string: The prompt to display ---@field actions table: A table with the actions to pick from -local select = require('CopilotChat.select') local chat = require('CopilotChat') +local utils = require('CopilotChat.utils') local M = {} ---- Diagnostic help actions ----@param config CopilotChat.config?: The chat configuration ----@return CopilotChat.integrations.actions?: The help actions -function M.help_actions(config) - local bufnr = vim.api.nvim_get_current_buf() - local winnr = vim.api.nvim_get_current_win() - local cursor = vim.api.nvim_win_get_cursor(winnr) - local line_diagnostics = vim.diagnostic.get(bufnr, { lnum = cursor[1] - 1 }) - - if #line_diagnostics == 0 then - return nil - end - - return { - prompt = 'Copilot Chat Help Actions', - actions = { - ['Fix diagnostic'] = vim.tbl_extend('keep', { - prompt = 'Please assist with fixing the following diagnostic issue in file:', - selection = select.diagnostics, - }, config or {}), - ['Explain diagnostic'] = vim.tbl_extend('keep', { - prompt = 'Please explain the following diagnostic issue in file:', - selection = select.diagnostics, - }, config or {}), - }, - } +function M.help_actions() + utils.deprecate('help_actions()', 'prompt_actions()') + return M.prompt_actions() end --- User prompt actions diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index e8993d9e..bf7fabb0 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -5,15 +5,23 @@ local select = require('CopilotChat.select') --- @field bufnr number --- @field winnr number +---@class CopilotChat.config.selection.diagnostic +---@field message string +---@field severity string +---@field start_row number +---@field start_col number +---@field end_row number +---@field end_col number + ---@class CopilotChat.config.selection ---@field lines string +---@field diagnostics table? ---@field filename string? ---@field filetype string? ---@field start_row number? ---@field start_col number? ---@field end_row number? ---@field end_col number? ----@field prompt_extra string? ---@class CopilotChat.config.prompt ---@field prompt string? @@ -106,15 +114,15 @@ return { history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received - -- default selection (visual or line) + -- default selection selection = function(source) - return select.visual(source) or select.line(source) + return select.visual(source) or select.buffer(source) end, -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection as paragraphs of text.', + prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection and diagnostics as paragraphs of text.', }, Review = { prompt = '/COPILOT_REVIEW Review the selected code.', @@ -170,10 +178,6 @@ return { Tests = { prompt = '/COPILOT_GENERATE Please generate tests for my code.', }, - FixDiagnostic = { - prompt = 'Please assist with the following diagnostic issue in file:', - selection = select.diagnostics, - }, Commit = { prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', selection = select.gitdiff, diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 7db68afa..d999a7cc 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -5,7 +5,7 @@ ---@field content string? ---@class CopilotChat.copilot.ask.opts ----@field selection string? +---@field selection CopilotChat.config.selection? ---@field embeddings table? ---@field filename string? ---@field filetype string? @@ -152,24 +152,56 @@ local function get_cached_token() return nil end -local function generate_selection_message(filename, filetype, start_row, end_row, selection) - if not selection or selection == '' then +local function generate_selection_message(filename, filetype, selection) + local content = selection.lines + + if not content or content == '' then return '' end - local content = selection - if start_row > 0 then - local lines = vim.split(selection, '\n') + if selection.start_row and selection.start_row > 0 then + local lines = vim.split(content, '\n') local total_lines = #lines local max_length = #tostring(total_lines) for i, line in ipairs(lines) do - local formatted_line_number = string.format('%' .. max_length .. 'd', i - 1 + start_row) + local formatted_line_number = + string.format('%' .. max_length .. 'd', i - 1 + selection.start_row) lines[i] = formatted_line_number .. ': ' .. line end content = table.concat(lines, '\n') end - return string.format('Active selection: `%s`\n```%s\n%s\n```', filename, filetype, content) + local out = string.format('Active selection: `%s`\n```%s\n%s\n```', filename, filetype, content) + + if selection.diagnostics then + local diagnostics = {} + for _, diagnostic in ipairs(selection.diagnostics) do + local start_row = diagnostic.start_row + local end_row = diagnostic.end_row + if start_row == end_row then + table.insert( + diagnostics, + string.format('%s line=%d: %s', diagnostic.severity, start_row, diagnostic.message) + ) + else + table.insert( + diagnostics, + string.format( + '%s line=%d-%d: %s', + diagnostic.severity, + start_row, + end_row, + diagnostic.message + ) + ) + end + end + + out = + string.format('%s\nDiagnostics: `%s`\n%s\n', out, filename, table.concat(diagnostics, '\n')) + end + + return out end local function generate_embeddings_message(embeddings) @@ -473,9 +505,7 @@ function Copilot:ask(prompt, opts) local embeddings = opts.embeddings or {} local filename = opts.filename or '' local filetype = opts.filetype or '' - local selection = opts.selection or '' - local start_row = opts.start_row or 0 - local end_row = opts.end_row or 0 + local selection = opts.selection or {} local system_prompt = opts.system_prompt or prompts.COPILOT_INSTRUCTIONS local model = opts.model or 'gpt-4o-2024-05-13' local temperature = opts.temperature or 0.1 @@ -484,7 +514,7 @@ function Copilot:ask(prompt, opts) self.current_job = job_id log.trace('System prompt: ' .. system_prompt) - log.trace('Selection: ' .. selection) + log.trace('Selection: ' .. (selection.lines or '')) log.debug('Prompt: ' .. prompt) log.debug('Embeddings: ' .. #embeddings) log.debug('Filename: ' .. filename) @@ -501,8 +531,7 @@ function Copilot:ask(prompt, opts) log.debug('Tokenizer: ' .. tokenizer) tiktoken_load(tokenizer) - local selection_message = - generate_selection_message(filename, filetype, start_row, end_row, selection) + local selection_message = generate_selection_message(filename, filetype, selection) local embeddings_message = generate_embeddings_message(embeddings) -- Count required tokens that we cannot reduce diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 5adf9efc..ad969b43 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -401,11 +401,11 @@ function M.ask(prompt, config, source) message = message:match('^%s*(.-)%s*$') return message end - return tostring(err) + return vim.inspect(err) end local function on_error(err) - log.error(err) + log.error(vim.inspect(err)) vim.schedule(function() append('\n\n' .. config.error_header .. config.separator .. '\n\n', config) append('```\n' .. get_error_message(err) .. '\n```', config) @@ -417,28 +417,15 @@ function M.ask(prompt, config, source) local selection = get_selection() local filetype = selection.filetype or (vim.api.nvim_buf_is_valid(state.source.bufnr) and vim.bo[state.source.bufnr].filetype) + or 'text' local filename = selection.filename - or ( - vim.api.nvim_buf_is_valid(state.source.bufnr) - and vim.api.nvim_buf_get_name(state.source.bufnr) - ) - - if not filetype then - on_error('No filetype found for the current buffer') - return - end - - if not filename then - on_error('No filename found for the current buffer') - return - end + or (vim.api.nvim_buf_is_valid(state.source.bufnr) and vim.api.nvim_buf_get_name( + state.source.bufnr + )) + or 'untitled' state.last_system_prompt = system_prompt - if selection.prompt_extra then - updated_prompt = updated_prompt .. ' ' .. selection.prompt_extra - end - if state.copilot:stop() then append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) end @@ -471,12 +458,10 @@ function M.ask(prompt, config, source) local ask_ok, response, token_count, token_max_count = pcall(state.copilot.ask, state.copilot, updated_prompt, { - selection = selection.lines, + selection = selection, embeddings = embeddings, filename = filename, filetype = filetype, - start_row = selection.start_row, - end_row = selection.end_row, system_prompt = system_prompt, model = config.model, temperature = config.temperature, @@ -617,34 +602,34 @@ end --- Set up the plugin ---@param config CopilotChat.config|nil function M.setup(config) - -- Handle old mapping format and show error - local found_old_format = false + -- Handle changed configuration if config then if config.mappings then for name, key in pairs(config.mappings) do if type(key) == 'string' then - vim.notify( - 'config.mappings.' - .. name - .. ": 'mappings' format have changed, please update your configuration, for now revering to default settings. See ':help CopilotChat-configuration' for current format", - vim.log.levels.ERROR + utils.deprecate( + 'config.mappings.' .. name, + 'config.mappings.' .. name .. '.normal and config.mappings.' .. name .. '.insert' ) - found_old_format = true + + config.mappings[name] = { + normal = key, + } end end end if config.yank_diff_register then - vim.notify( - 'config.yank_diff_register: This option has been removed, please use mappings.yank_diff.register instead', - vim.log.levels.ERROR - ) + utils.deprecate('config.yank_diff_register', 'config.mappings.yank_diff.register') + config.mappings.yank_diff.register = config.yank_diff_register end end - if found_old_format then - config.mappings = nil - end + -- Handle removed commands + vim.api.nvim_create_user_command('CopilotChatFixDiagnostic', function() + utils.deprecate('CopilotChatFixDiagnostic', 'CopilotChatFix') + M.ask('/Fix') + end, { force = true }) M.config = vim.tbl_deep_extend('force', default_config, config or {}) if M.config.model == 'gpt-4o' then @@ -951,7 +936,6 @@ function M.setup(config) }) vim.api.nvim_create_user_command('CopilotChatModel', function() - -- Show which model is being used in an alert vim.notify('Using model: ' .. M.config.model, vim.log.levels.INFO) end, { force = true }) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index cb958721..82e25b7b 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -1,5 +1,32 @@ local M = {} +local function get_diagnostics_in_range(bufnr, start_line, end_line) + local diagnostics = vim.diagnostic.get(bufnr) + local range_diagnostics = {} + local severity = { + [1] = 'ERROR', + [2] = 'WARNING', + [3] = 'INFORMATION', + [4] = 'HINT', + } + + for _, diagnostic in ipairs(diagnostics) do + local lnum = diagnostic.lnum + 1 + if lnum >= start_line and lnum <= end_line then + table.insert(range_diagnostics, { + message = diagnostic.message, + severity = severity[diagnostic.severity], + start_row = lnum, + start_col = diagnostic.col + 1, + end_row = lnum, + end_col = diagnostic.end_col and (diagnostic.end_col + 1) or diagnostic.col + 1, + }) + end + end + + return #range_diagnostics > 0 and range_diagnostics or nil +end + local function get_selection_lines(bufnr, start_line, start_col, finish_line, finish_col, full_line) -- Exit if no actual selection if start_line == finish_line and start_col == finish_col then @@ -68,34 +95,6 @@ function M.visual(source) return get_selection_lines(bufnr, start_line, start_col, finish_line, finish_col, false) end ---- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. ---- @return CopilotChat.config.selection|nil -function M.unnamed() - local lines = vim.fn.getreg('"') - - if not lines or lines == '' then - return nil - end - - return { - lines = lines, - } -end - ---- Select and process contents of plus register (+). This register is synchronized with system clipboard. ---- @return CopilotChat.config.selection|nil -function M.clipboard() - local lines = vim.fn.getreg('+') - - if not lines or lines == '' then - return nil - end - - return { - lines = lines, - } -end - --- Select and process whole buffer --- @param source CopilotChat.config.source --- @return CopilotChat.config.selection|nil @@ -107,13 +106,16 @@ function M.buffer(source) return nil end - return { + local out = { lines = table.concat(lines, '\n'), start_row = 1, start_col = 1, end_row = #lines, end_col = #lines[#lines], } + + out.diagnostics = get_diagnostics_in_range(bufnr, out.start_row, out.end_row) + return out end --- Select and process current line @@ -129,45 +131,44 @@ function M.line(source) return nil end - return { + local out = { lines = line, start_row = cursor[1], start_col = 1, end_row = cursor[1], end_col = #line, } + + out.diagnostics = get_diagnostics_in_range(bufnr, out.start_row, out.end_row) + return out end ---- Select whole buffer and find diagnostics ---- It uses the built-in LSP client in Neovim to get the diagnostics. ---- @param source CopilotChat.config.source +--- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. --- @return CopilotChat.config.selection|nil -function M.diagnostics(source) - local bufnr = source.bufnr - local winnr = source.winnr - local select_buffer = M.buffer(source) - if not select_buffer then +function M.unnamed() + local lines = vim.fn.getreg('"') + + if not lines or lines == '' then return nil end - local cursor = vim.api.nvim_win_get_cursor(winnr) - local line_diagnostics = vim.diagnostic.get(bufnr, { lnum = cursor[1] - 1 }) + return { + lines = lines, + } +end - if #line_diagnostics == 0 then - return nil - end +--- Select and process contents of plus register (+). This register is synchronized with system clipboard. +--- @return CopilotChat.config.selection|nil +function M.clipboard() + local lines = vim.fn.getreg('+') - local diagnostics = {} - for _, diagnostic in ipairs(line_diagnostics) do - table.insert(diagnostics, diagnostic.message) + if not lines or lines == '' then + return nil end - local result = table.concat(diagnostics, '. ') - result = result:gsub('^%s*(.-)%s*$', '%1'):gsub('\n', ' ') - - local file_name = vim.api.nvim_buf_get_name(bufnr) - select_buffer.prompt_extra = file_name .. ':' .. cursor[1] .. '. ' .. result - return select_buffer + return { + lines = lines, + } end --- Select and process current git diff diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index abc07126..2a1e6ef3 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -90,4 +90,8 @@ function M.return_to_normal_mode() end end +function M.deprecate(old, new) + vim.deprecate(old, new, '3.0.0', 'CopilotChat.nvim', false) +end + return M From 19bdf3aab11470f87adf69cebaaa45fbe00a83dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 15 Nov 2024 11:49:21 +0000 Subject: [PATCH 121/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 3d4cc673..dcb6b21f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -142,13 +142,12 @@ COMMANDS ~ COMMANDS COMING FROM DEFAULT PROMPTS -- `:CopilotChatExplain` - Write an explanation for the active selection as paragraphs of text +- `:CopilotChatExplain` - Write an explanation for the active selection and diagnostics as paragraphs of text - `:CopilotChatReview` - Review the selected code - `:CopilotChatFix` - There is a problem in this code. Rewrite the code to show it with the bug fixed - `:CopilotChatOptimize` - Optimize the selected code to improve performance and readability - `:CopilotChatDocs` - Please add documentation comment for the selection - `:CopilotChatTests` - Please generate tests for my code -- `:CopilotChatFixDiagnostic` - Please assist with the following diagnostic issue in file - `:CopilotChatCommit` - Write commit message for the change with commitizen convention - `:CopilotChatCommitStaged` - Write commit message for the change with commitizen convention @@ -210,9 +209,6 @@ API ~ -- Pick a prompt using vim.ui.select local actions = require("CopilotChat.actions") - -- Pick help actions - actions.pick(actions.help_actions()) - -- Pick prompt actions actions.pick(actions.prompt_actions({ selection = require("CopilotChat.select").visual, @@ -258,15 +254,15 @@ Also see here : history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received - -- default selection (visual or line) + -- default selection selection = function(source) - return select.visual(source) or select.line(source) + return select.visual(source) or select.buffer(source) end, -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection as paragraphs of text.', + prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection and diagnostics as paragraphs of text.', }, Review = { prompt = '/COPILOT_REVIEW Review the selected code.', @@ -286,10 +282,6 @@ Also see here : Tests = { prompt = '/COPILOT_GENERATE Please generate tests for my code.', }, - FixDiagnostic = { - prompt = 'Please assist with the following diagnostic issue in file:', - selection = select.diagnostics, - }, Commit = { prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', selection = select.gitdiff, @@ -501,15 +493,6 @@ plugin to be installed. >lua -- lazy.nvim keys - -- Show help actions with telescope - { - "cch", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.telescope").pick(actions.help_actions()) - end, - desc = "CopilotChat - Help actions", - }, -- Show prompts actions with telescope { "ccp", @@ -528,15 +511,6 @@ Requires fzf-lua plugin to be installed. >lua -- lazy.nvim keys - -- Show help actions with fzf-lua - { - "cch", - function() - local actions = require("CopilotChat.actions") - require("CopilotChat.integrations.fzflua").pick(actions.help_actions()) - end, - desc = "CopilotChat - Help actions", - }, -- Show prompts actions with fzf-lua { "ccp", From 01f79f81b5bed4a3e4572fc48d583dc1929ab38a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Nov 2024 12:52:11 +0100 Subject: [PATCH 122/338] refactor(commit): consolidate commit commands into one Simplifies the commit command functionality by making staged changes the default behavior. Removes separate command and configuration for staged changes in favor of a single unified approach. - Removes CopilotChatCommitStaged command configuration - Makes staged changes the default in gitdiff function - Adds deprecated command handler for backward compatibility --- README.md | 7 ------- lua/CopilotChat/config.lua | 6 ------ lua/CopilotChat/init.lua | 4 ++++ lua/CopilotChat/select.lua | 5 ++--- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6157151a..7959cdeb 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,6 @@ Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabl - `:CopilotChatDocs` - Please add documentation comment for the selection - `:CopilotChatTests` - Please generate tests for my code - `:CopilotChatCommit` - Write commit message for the change with commitizen convention -- `:CopilotChatCommitStaged` - Write commit message for the change with commitizen convention ### API @@ -255,12 +254,6 @@ Also see [here](/lua/CopilotChat/config.lua): prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', selection = select.gitdiff, }, - CommitStaged = { - prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', - selection = function(source) - return select.gitdiff(source, true) - end, - }, }, -- default window options diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index bf7fabb0..aa17fc3b 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -182,12 +182,6 @@ return { prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', selection = select.gitdiff, }, - CommitStaged = { - prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', - selection = function(source) - return select.gitdiff(source, true) - end, - }, }, -- default window options diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ad969b43..24473e41 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -630,6 +630,10 @@ function M.setup(config) utils.deprecate('CopilotChatFixDiagnostic', 'CopilotChatFix') M.ask('/Fix') end, { force = true }) + vim.api.nvim_create_user_command('CopilotChatCommitStaged', function() + utils.deprecate('CopilotChatCommitStaged', 'CopilotChatCommit') + M.ask('/Commit') + end, { force = true }) M.config = vim.tbl_deep_extend('force', default_config, config or {}) if M.config.model == 'gpt-4o' then diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 82e25b7b..5dbb9f59 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -173,9 +173,8 @@ end --- Select and process current git diff --- @param source CopilotChat.config.source ---- @param staged boolean @If true, it will return the staged changes --- @return CopilotChat.config.selection|nil -function M.gitdiff(source, staged) +function M.gitdiff(source) local select_buffer = M.buffer(source) if not select_buffer then return nil @@ -189,7 +188,7 @@ function M.gitdiff(source, staged) end dir = dir:gsub('.git$', '') - local cmd = 'git -C ' .. dir .. ' diff --no-color --no-ext-diff' .. (staged and ' --staged' or '') + local cmd = 'git -C ' .. dir .. ' diff --no-color --no-ext-diff --staged' local handle = io.popen(cmd) if not handle then return nil From 683e07edc1b191dbb60a55ddd31bc92076eb41c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 15 Nov 2024 11:58:49 +0000 Subject: [PATCH 123/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index dcb6b21f..2e86594d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -149,7 +149,6 @@ COMMANDS COMING FROM DEFAULT PROMPTS - `:CopilotChatDocs` - Please add documentation comment for the selection - `:CopilotChatTests` - Please generate tests for my code - `:CopilotChatCommit` - Write commit message for the change with commitizen convention -- `:CopilotChatCommitStaged` - Write commit message for the change with commitizen convention API ~ @@ -286,12 +285,6 @@ Also see here : prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', selection = select.gitdiff, }, - CommitStaged = { - prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', - selection = function(source) - return select.gitdiff(source, true) - end, - }, }, -- default window options From 76bfba5cd8915ff55554e0649809bec0207b6a83 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Nov 2024 13:45:51 +0100 Subject: [PATCH 124/338] Show only error message from curl instead of whole object Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 6 ++++-- lua/CopilotChat/init.lua | 37 ++++++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index d999a7cc..3153604f 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -59,7 +59,8 @@ local curl_get = async.wrap(function(url, opts, callback) opts = vim.tbl_deep_extend('force', opts, { callback = callback, on_error = function(err) - callback(nil, vim.inspect(err)) + err = err and err.stderr or vim.inspect(err) + callback(nil, err) end, }) curl.get(url, opts) @@ -69,7 +70,8 @@ local curl_post = async.wrap(function(url, opts, callback) opts = vim.tbl_deep_extend('force', opts, { callback = callback, on_error = function(err) - callback(nil, vim.inspect(err)) + err = err and err.stderr or vim.inspect(err) + callback(nil, err) end, }) curl.post(url, opts) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 24473e41..bddca5bc 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -60,6 +60,32 @@ local function blend_color_with_neovim_bg(color_name, blend) return string.format('#%02x%02x%02x', r, g, b) end +local function dedupe_strings(str) + if not str then + return str + end + local seen = {} + local result = {} + for s in str:gmatch('[^%s,]+') do + if not seen[s] then + seen[s] = true + table.insert(result, s) + end + end + return table.concat(result, ' ') +end + +local function get_error_message(err) + if type(err) == 'string' then + -- Match first occurrence of :something: and capture rest + local message = err:match('^[^:]+:[^:]+:(.+)') or err + -- Trim whitespace + message = message:match('^%s*(.-)%s*$') + return dedupe_strings(message) + end + return dedupe_strings(vim.inspect(err)) +end + local function find_lines_between_separator(lines, pattern, at_least_one) local line_count = #lines local separator_line_start = 1 @@ -393,17 +419,6 @@ function M.ask(prompt, config, source) M.stop(true, config) end - local function get_error_message(err) - if type(err) == 'string' then - -- Match first occurrence of :something: and capture rest - local message = err:match('^[^:]+:[^:]+:(.+)') or err - -- Trim whitespace - message = message:match('^%s*(.-)%s*$') - return message - end - return vim.inspect(err) - end - local function on_error(err) log.error(vim.inspect(err)) vim.schedule(function() From 879c59d853876c160f6c70d18c8343d3243d12a8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Nov 2024 01:49:01 +0100 Subject: [PATCH 125/338] Increase flexibility of diff selectors This change allows selecting different diffs by simply navigating cursor to the diff codeblock. Potentionally closes #318 Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 81 +++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index bddca5bc..7c3c4cf2 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -86,37 +86,61 @@ local function get_error_message(err) return dedupe_strings(vim.inspect(err)) end -local function find_lines_between_separator(lines, pattern, at_least_one) +local function find_lines_between_separator( + lines, + start_line, + start_pattern, + end_pattern, + allow_end_of_file +) + if not end_pattern then + end_pattern = start_pattern + end + local line_count = #lines + local current_line = vim.api.nvim_win_get_cursor(0)[1] - start_line local separator_line_start = 1 local separator_line_finish = line_count local found_one = false - -- Find the last occurrence of the separator - for i = line_count, 1, -1 do -- Reverse the loop to start from the end + -- Find starting separator line + for i = current_line, 1, -1 do local line = lines[i] - if string.find(line, pattern) then - if i < (separator_line_finish + 1) and (not at_least_one or found_one) then - separator_line_start = i + 1 - break -- Exit the loop as soon as the condition is met + + if line and string.match(line, start_pattern) then + separator_line_start = i + 1 + + for x = separator_line_start, line_count do + local next_line = lines[x] + if next_line and string.match(next_line, end_pattern) then + separator_line_finish = x - 1 + found_one = true + break + end + if allow_end_of_file and x == line_count then + separator_line_finish = x + found_one = true + break + end end - found_one = true - separator_line_finish = i - 1 + if found_one then + break + end end end - if at_least_one and not found_one then - return {}, 1, 1, 0 + if not found_one then + return {}, 1, 1 end - -- Extract everything between the last and next separator + -- Extract everything between the last and next separator or end of file local result = {} for i = separator_line_start, separator_line_finish do table.insert(result, lines[i]) end - return result, separator_line_start, separator_line_finish, line_count + return result, separator_line_start, separator_line_finish end local function update_prompts(prompt, system_prompt) @@ -790,14 +814,15 @@ function M.setup(config) map_key(M.config.mappings.submit_prompt, bufnr, function() local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local lines, start_line, end_line, line_count = - find_lines_between_separator(chat_lines, M.config.separator .. '$') + local lines, start_line, end_line = + find_lines_between_separator(chat_lines, 0, M.config.separator .. '$', nil, true) local input = vim.trim(table.concat(lines, '\n')) if input ~= '' then -- If we are entering the input at the end, replace it - if line_count == end_line then + if #chat_lines == end_line then vim.api.nvim_buf_set_lines(bufnr, start_line, end_line, false, { '' }) end + M.ask(input, state.config, state.source) end end) @@ -809,9 +834,10 @@ function M.setup(config) end local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local section_lines = - find_lines_between_separator(chat_lines, M.config.separator .. '$', true) - local lines = find_lines_between_separator(section_lines, '^```%w*$', true) + local section_lines, start_line = + find_lines_between_separator(chat_lines, 0, M.config.separator .. '$') + local lines = + find_lines_between_separator(section_lines, start_line - 1, '^```%w+$', '^```$') if #lines > 0 then vim.api.nvim_buf_set_text( state.source.bufnr, @@ -831,9 +857,10 @@ function M.setup(config) end local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local section_lines = - find_lines_between_separator(chat_lines, M.config.separator .. '$', true) - local lines = find_lines_between_separator(section_lines, '^```%w*$', true) + local section_lines, start_line = + find_lines_between_separator(chat_lines, 0, M.config.separator .. '$') + local lines = + find_lines_between_separator(section_lines, start_line - 1, '^```%w+$', '^```$') if #lines > 0 then local content = table.concat(lines, '\n') vim.fn.setreg(M.config.mappings.yank_diff.register, content) @@ -847,10 +874,12 @@ function M.setup(config) end local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) - local section_lines = - find_lines_between_separator(chat_lines, M.config.separator .. '$', true) - local lines = - table.concat(find_lines_between_separator(section_lines, '^```%w*$', true), '\n') + local section_lines, start_line = + find_lines_between_separator(chat_lines, 0, M.config.separator .. '$') + local lines = table.concat( + find_lines_between_separator(section_lines, start_line - 1, '^```%w+$', '^```$'), + '\n' + ) if vim.trim(lines) ~= '' then state.last_code_output = lines From b555e557b39dd05e47ddd1b85f82d4df55405976 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 01:18:36 +0000 Subject: [PATCH 126/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 2e86594d..acf49a11 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 15 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 16 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 0db854473aa5e25682c168809edb5ad9878a9651 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Nov 2024 02:40:16 +0100 Subject: [PATCH 127/338] Automatically register copilot-chat for treesitter markdown And add render-markdown integration info Signed-off-by: Tomas Slusny --- README.md | 15 +++++++++++++++ lua/CopilotChat/chat.lua | 2 ++ 2 files changed, 17 insertions(+) diff --git a/README.md b/README.md index 7959cdeb..5bd899e4 100644 --- a/README.md +++ b/README.md @@ -509,6 +509,21 @@ require('CopilotChat').setup({ +
+render-markdown integration + +Requires [render-markdown](https://github.com/MeanderingProgrammer/render-markdown.nvim) plugin to be installed. + +```lua +require('render-markdown').setup({ + file_types = { 'markdown', 'copilot-chat' }, +}) +``` + +![image](https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8) + +
+ ## Roadmap (Wishlist) - Use indexed vector database with current workspace for better context selection diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 078cf516..2ef04851 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -43,6 +43,8 @@ local Chat = class(function(self, help, auto_insert, on_buf_create) self.separator = nil self.layout = nil + vim.treesitter.language.register('markdown', 'copilot-chat') + self.buf_create = function() local bufnr = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_name(bufnr, 'copilot-chat') From a1d97c78bad7e737d6dfedb92a8ab7a3e9ea4de3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 01:44:22 +0000 Subject: [PATCH 128/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index acf49a11..9471aa43 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -535,6 +535,18 @@ Requires nvim-cmp plugin to be installed }) < +render-markdown integration ~ + +Requires render-markdown + plugin to be +installed. + +>lua + require('render-markdown').setup({ + file_types = { 'markdown', 'copilot-chat' }, + }) +< + ROADMAP (WISHLIST) *CopilotChat-roadmap-(wishlist)* @@ -589,7 +601,8 @@ STARGAZERS OVER TIME ~ 10. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b 11. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 12. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/063fc99f-a4b2-4187-a065-0fdd287ebee2 -13. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg +13. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 +14. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From c98301c5cbba79b70beffa9699ce89c75691c093 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Nov 2024 19:25:26 +0100 Subject: [PATCH 129/338] Add support for copilot extension agents https://docs.github.com/en/copilot/building-copilot-extensions/about-building-copilot-extensions - Change @buffer and @buffers to #buffer and #buffers - Add support for @agent agent selection - Add support for config.agent for specifying default agent - Add :CopilotChatAgents for listing agents (and showing selected agent) - Remove :CopilotChatModel, instead show which model is selected in :CopilotChatModels - Remove early errors from curl so we can actually get response body for the error - Add info to README about models, agents and contexts Closes #466 Signed-off-by: Tomas Slusny --- README.md | 42 ++++- lua/CopilotChat/config.lua | 10 +- lua/CopilotChat/copilot.lua | 116 +++++++++++++- lua/CopilotChat/init.lua | 221 +++++++++++++++++---------- lua/CopilotChat/integrations/cmp.lua | 36 ++--- 5 files changed, 308 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 5bd899e4..c1b8e2aa 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabl - `:CopilotChatLoad ?` - Load chat history from file - `:CopilotChatDebugInfo` - Show debug information - `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. -- `:CopilotChatModel` - View the currently selected model. +- `:CopilotChatAgents` - View and select available agents. This is reset when a new instance is made. Please set your agent in `init.lua` for persistence. #### Commands coming from default prompts @@ -122,6 +122,39 @@ Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabl - `:CopilotChatTests` - Please generate tests for my code - `:CopilotChatCommit` - Write commit message for the change with commitizen convention +### Models, Agents and Contexts + +#### Models + +You can list available models with `:CopilotChatModels` command. Model determines the AI model used for the chat. +Default models are: + +- `gpt-4o` - This is the default Copilot Chat model. It is a versatile, multimodal model that excels in both text and image processing and is designed to provide fast, reliable responses. It also has superior performance in non-English languages. Gpt-4o is hosted on Azure. +- `claude-3.5-sonnet` - This model excels at coding tasks across the entire software development lifecycle, from initial design to bug fixes, maintenance to optimizations. GitHub Copilot uses Claude 3.5 Sonnet hosted on Amazon Web Services. +- `o1-preview` - This model is focused on advanced reasoning and solving complex problems, in particular in math and science. It responds more slowly than the gpt-4o model. You can make 10 requests to this model per day. o1-preview is hosted on Azure. +- `o1-mini` - This is the faster version of the o1-preview model, balancing the use of complex reasoning with the need for faster responses. It is best suited for code generation and small context operations. You can make 50 requests to this model per day. o1-mini is hosted on Azure. + +For more information about models, see [here](https://docs.github.com/en/copilot/using-github-copilot/asking-github-copilot-questions-in-your-ide#ai-models-for-copilot-chat) +You can use more models from [here](https://github.com/marketplace/models) by using `@models` agent from [here](https://github.com/marketplace/models-github) (example: `@models Using Mistral-small, what is 1 + 11`) + +#### Agents + +Agents are used to determine the AI agent used for the chat. You can list available agents with `:CopilotChatAgents` command. +You can set the agent in the prompt by using `@` followed by the agent name. +Default "noop" agent is `copilot`. + +For more information about extension agents, see [here](https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat) +You can install more agents from [here](https://github.com/marketplace?type=apps&copilot_app=true) + +#### Contexts + +Contexts are used to determine the context of the chat. +You can set the context in the prompt by using `#` followed by the context name. +Supported contexts are: + +- `buffers` - Includes all open buffers in chat context +- `buffer` - Includes only the current buffer in chat context + ### API ```lua @@ -202,8 +235,10 @@ Also see [here](/lua/CopilotChat/config.lua): allow_insecure = false, -- Allow insecure server connections system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4o', -- GPT model to use, see ':CopilotChatModels' for available models - temperature = 0.1, -- GPT temperature + model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models + agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). + context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via #). + temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions answer_header = '## Copilot ', -- Header to use for AI answers @@ -218,7 +253,6 @@ Also see [here](/lua/CopilotChat/config.lua): clear_chat_on_new_prompt = false, -- Clears chat on every new prompt highlight_selection = true, -- Highlight selection in the source buffer when in the chat window - context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via @). history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index aa17fc3b..7d736f10 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -69,6 +69,8 @@ local select = require('CopilotChat.select') ---@field allow_insecure boolean? ---@field system_prompt string? ---@field model string? +---@field agent string? +---@field context string? ---@field temperature number? ---@field question_header string? ---@field answer_header string? @@ -80,7 +82,6 @@ local select = require('CopilotChat.select') ---@field auto_insert_mode boolean? ---@field clear_chat_on_new_prompt boolean? ---@field highlight_selection boolean? ----@field context string? ---@field history_path string? ---@field callback fun(response: string, source: CopilotChat.config.source)? ---@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.config.selection? @@ -94,8 +95,10 @@ return { allow_insecure = false, -- Allow insecure server connections system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4o', -- GPT model to use, see ':CopilotChatModels' for available models - temperature = 0.1, -- GPT temperature + model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models + agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). + context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via #). + temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions answer_header = '## Copilot ', -- Header to use for AI answers @@ -110,7 +113,6 @@ return { clear_chat_on_new_prompt = false, -- Clears chat on every new prompt highlight_selection = true, -- Highlight selection - context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via @). history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 3153604f..733d3119 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -13,6 +13,7 @@ ---@field end_row number? ---@field system_prompt string? ---@field model string? +---@field agent string? ---@field temperature number? ---@field on_progress nil|fun(response: string):nil @@ -29,6 +30,7 @@ ---@field load fun(self: CopilotChat.Copilot, name: string, path: string):table ---@field running fun(self: CopilotChat.Copilot):boolean ---@field list_models fun(self: CopilotChat.Copilot):table +---@field list_agents fun(self: CopilotChat.Copilot):table local async = require('plenary.async') local log = require('plenary.log') @@ -340,6 +342,7 @@ local Copilot = class(function(self, proxy, allow_insecure) self.sessionid = nil self.machineid = machine_id() self.models = nil + self.agents = nil self.claude_enabled = false self.current_job = nil self.request_args = { @@ -362,9 +365,6 @@ local Copilot = class(function(self, proxy, allow_insecure) '--no-keepalive', -- Don't reuse connections '--tcp-nodelay', -- Disable Nagle's algorithm for faster streaming '--no-buffer', -- Disable output buffering for streaming - '--fail', -- Return error on HTTP errors (4xx, 5xx) - '--silent', -- Don't show progress meter - '--show-error', -- Show errors even when silent }, } end) @@ -461,6 +461,39 @@ function Copilot:fetch_models() return out end +function Copilot:fetch_agents() + if self.agents then + return self.agents + end + + local response, err = curl_get( + 'https://api.githubcopilot.com/agents', + vim.tbl_extend('force', self.request_args, { + headers = self:authenticate(), + }) + ) + + if err then + error(err) + end + + if response.status ~= 200 then + error('Failed to fetch agents: ' .. tostring(response.status)) + end + + local agents = vim.json.decode(response.body)['agents'] + local out = {} + for _, agent in ipairs(agents) do + out[agent['slug']] = agent + end + + out['copilot'] = { name = 'Copilot', default = true } + + log.info('Agents fetched') + self.agents = out + return out +end + function Copilot:enable_claude() if self.claude_enabled then return true @@ -510,6 +543,7 @@ function Copilot:ask(prompt, opts) local selection = opts.selection or {} local system_prompt = opts.system_prompt or prompts.COPILOT_INSTRUCTIONS local model = opts.model or 'gpt-4o-2024-05-13' + local agent = opts.agent or 'copilot' local temperature = opts.temperature or 0.1 local on_progress = opts.on_progress local job_id = uuid() @@ -522,10 +556,21 @@ function Copilot:ask(prompt, opts) log.debug('Filename: ' .. filename) log.debug('Filetype: ' .. filetype) log.debug('Model: ' .. model) + log.debug('Agent: ' .. agent) log.debug('Temperature: ' .. temperature) local models = self:fetch_models() - local capabilities = models[model] and models[model].capabilities + local agents = self:fetch_agents() + local agent_config = agents[agent] + if not agent_config then + error('Agent not found: ' .. agent) + end + local model_config = models[model] + if not model_config then + error('Model not found: ' .. model) + end + + local capabilities = model_config.capabilities local max_tokens = capabilities.limits.max_prompt_tokens -- FIXME: Is max_prompt_tokens the right limit? local max_output_tokens = capabilities.limits.max_output_tokens local tokenizer = capabilities.tokenizer @@ -582,6 +627,7 @@ function Copilot:ask(prompt, opts) local errored = false local finished = false local full_response = '' + local full_references = '' local function finish_stream(err, job) if err then @@ -631,6 +677,22 @@ function Copilot:ask(prompt, opts) return end + if content.copilot_references then + for _, reference in ipairs(content.copilot_references) do + local metadata = reference.metadata + if metadata and metadata.display_name and metadata.display_url then + full_references = full_references + .. '\n' + .. '[' + .. metadata.display_name + .. ']' + .. '(' + .. metadata.display_url + .. ')' + end + end + end + if not content.choices or #content.choices == 0 then return end @@ -668,8 +730,13 @@ function Copilot:ask(prompt, opts) self:enable_claude() end + local url = 'https://api.githubcopilot.com/chat/completions' + if not agent_config.default then + url = 'https://api.githubcopilot.com/agents/' .. agent .. '?chat' + end + local response, err = curl_post( - 'https://api.githubcopilot.com/chat/completions', + url, vim.tbl_extend('force', self.request_args, { headers = self:authenticate(), body = temp_file(body), @@ -694,6 +761,25 @@ function Copilot:ask(prompt, opts) end if response.status ~= 200 then + if response.status == 401 then + local ok, content = pcall(vim.json.decode, response.body, { + luanil = { + object = true, + array = true, + }, + }) + + if ok and content.authorize_url then + error( + 'Failed to authenticate. Visit following url to authorize ' + .. content.slug + .. ':\n' + .. content.authorize_url + ) + return + end + end + error('Failed to get response: ' .. tostring(response.status) .. '\n' .. response.body) return end @@ -708,6 +794,14 @@ function Copilot:ask(prompt, opts) return end + if full_references ~= '' then + full_references = '\n\n**`References:`**' .. full_references + full_response = full_response .. full_references + if on_progress then + on_progress(full_references) + end + end + log.trace('Full response: ' .. full_response) log.debug('Last message: ' .. vim.inspect(last_message)) @@ -727,10 +821,10 @@ function Copilot:ask(prompt, opts) end --- List available models +---@return table function Copilot:list_models() local models = self:fetch_models() - -- Group models by version and shortest ID local version_map = {} for id, model in pairs(models) do local version = model.version @@ -739,10 +833,18 @@ function Copilot:list_models() end end - -- Map to IDs and sort local result = vim.tbl_values(version_map) table.sort(result) + return result +end +--- List available agents +---@return table +function Copilot:list_agents() + local agents = self:fetch_agents() + + local result = vim.tbl_keys(agents) + table.sort(result) return result end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7c3c4cf2..ba381b39 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -60,30 +60,13 @@ local function blend_color_with_neovim_bg(color_name, blend) return string.format('#%02x%02x%02x', r, g, b) end -local function dedupe_strings(str) - if not str then - return str - end - local seen = {} - local result = {} - for s in str:gmatch('[^%s,]+') do - if not seen[s] then - seen[s] = true - table.insert(result, s) - end - end - return table.concat(result, ' ') -end - local function get_error_message(err) if type(err) == 'string' then - -- Match first occurrence of :something: and capture rest local message = err:match('^[^:]+:[^:]+:(.+)') or err - -- Trim whitespace - message = message:match('^%s*(.-)%s*$') - return dedupe_strings(message) + message = message:gsub('^%s*', '') + return message end - return dedupe_strings(vim.inspect(err)) + return vim.inspect(err) end local function find_lines_between_separator( @@ -182,58 +165,6 @@ local function append(str, config) end end -local function complete() - local line = vim.api.nvim_get_current_line() - local col = vim.api.nvim_win_get_cursor(0)[2] - if col == 0 or #line == 0 then - return - end - - local prefix, cmp_start = unpack(vim.fn.matchstrpos(line:sub(1, col), [[\(/\|@\)\k*$]])) - if not prefix then - return - end - - local items = {} - local prompts_to_use = M.prompts() - - for name, prompt in pairs(prompts_to_use) do - items[#items + 1] = { - word = '/' .. name, - kind = prompt.kind, - info = prompt.prompt, - menu = prompt.description or '', - icase = 1, - dup = 0, - empty = 0, - } - end - - items[#items + 1] = { - word = '@buffers', - kind = 'context', - menu = 'Use all loaded buffers as context', - icase = 1, - dup = 0, - empty = 0, - } - - items[#items + 1] = { - word = '@buffer', - kind = 'context', - menu = 'Use the current buffer as context', - icase = 1, - dup = 0, - empty = 0, - } - - items = vim.tbl_filter(function(item) - return vim.startswith(item.word:lower(), prefix:lower()) - end, items) - - vim.fn.complete(cmp_start + 1, items) -end - local function get_selection() local bufnr = state.source.bufnr local winnr = state.source.winnr @@ -302,6 +233,70 @@ local function key_to_info(name, key, surround) return out end +--- Get the completion info for the chat window, for use with custom completion providers +---@return table +function M.complete_info() + return { + triggers = { '@', '/', '#' }, + pattern = [[\%(@\|/\|#\)\k*]], + } +end + +--- Get the completion items for the chat window, for use with custom completion providers +---@param callback function(table) +function M.complete_items(callback) + async.run(function() + local agents = state.copilot:list_agents() + local items = {} + local prompts_to_use = M.prompts() + + for name, prompt in pairs(prompts_to_use) do + items[#items + 1] = { + word = '/' .. name, + kind = prompt.kind, + info = prompt.prompt, + menu = prompt.description or '', + icase = 1, + dup = 0, + empty = 0, + } + end + + for _, agent in ipairs(agents) do + items[#items + 1] = { + word = '@' .. agent, + kind = 'agent', + menu = 'Use the specified agent', + icase = 1, + dup = 0, + empty = 0, + } + end + + items[#items + 1] = { + word = '#buffers', + kind = 'context', + menu = 'Include all loaded buffers in context', + icase = 1, + dup = 0, + empty = 0, + } + + items[#items + 1] = { + word = '#buffer', + kind = 'context', + menu = 'Include the specified buffer in context', + icase = 1, + dup = 0, + empty = 0, + } + + vim.schedule(function() + callback(items) + end) + end) +end + --- Get the prompts to use. ---@param skip_system boolean|nil ---@return table @@ -408,13 +403,44 @@ end function M.select_model() async.run(function() local models = state.copilot:list_models() + models = vim.tbl_map(function(model) + if model == M.config.model then + return model .. ' (selected)' + end + + return model + end, models) vim.schedule(function() vim.ui.select(models, { prompt = 'Select a model', }, function(choice) if choice then - M.config.model = choice + M.config.model = choice:gsub(' %(selected%)', '') + end + end) + end) + end) +end + +--- Select a Copilot agent. +function M.select_agent() + async.run(function() + local agents = state.copilot:list_agents() + agents = vim.tbl_map(function(agent) + if agent == M.config.agent then + return agent .. ' (selected)' + end + + return agent + end, agents) + + vim.schedule(function() + vim.ui.select(agents, { + prompt = 'Select an agent', + }, function(choice) + if choice then + M.config.agent = choice:gsub(' %(selected%)', '') end end) end) @@ -473,14 +499,24 @@ function M.ask(prompt, config, source) append('\n\n' .. config.answer_header .. config.separator .. '\n\n', config) local selected_context = config.context - if string.find(prompt, '@buffers') then + if string.find(prompt, '#buffers') then selected_context = 'buffers' - elseif string.find(prompt, '@buffer') then + elseif string.find(prompt, '#buffer') then selected_context = 'buffer' end - updated_prompt = string.gsub(updated_prompt, '@buffers?%s*', '') + updated_prompt = string.gsub(updated_prompt, '#buffers?%s*', '') async.run(function() + local agents = state.copilot:list_agents() + local current_agent = config.agent + + for agent in updated_prompt:gmatch('@([%w_-]+)') do + if vim.tbl_contains(agents, agent) then + current_agent = agent + updated_prompt = updated_prompt:gsub('@' .. agent .. '%s*', '') + end + end + local query_ok, embeddings = pcall(context.find_for_query, state.copilot, { context = selected_context, prompt = updated_prompt, @@ -503,6 +539,7 @@ function M.ask(prompt, config, source) filetype = filetype, system_prompt = system_prompt, model = config.model, + agent = current_agent, temperature = config.temperature, on_progress = function(token) vim.schedule(function() @@ -808,10 +845,32 @@ function M.setup(config) state.help:show(chat_help, 'markdown', 'markdown', state.chat.winnr) end) - map_key(M.config.mappings.complete, bufnr, complete) map_key(M.config.mappings.reset, bufnr, M.reset) map_key(M.config.mappings.close, bufnr, M.close) + map_key(M.config.mappings.complete, bufnr, function() + local info = M.complete_info() + local line = vim.api.nvim_get_current_line() + local col = vim.api.nvim_win_get_cursor(0)[2] + if col == 0 or #line == 0 then + return + end + + local prefix, cmp_start = unpack(vim.fn.matchstrpos(line:sub(1, col), info.pattern)) + if not prefix then + return + end + + M.complete_items(function(items) + vim.fn.complete( + cmp_start + 1, + vim.tbl_filter(function(item) + return vim.startswith(item.word:lower(), prefix:lower()) + end, items) + ) + end) + end) + map_key(M.config.mappings.submit_prompt, bufnr, function() local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local lines, start_line, end_line = @@ -983,14 +1042,12 @@ function M.setup(config) range = true, }) - vim.api.nvim_create_user_command('CopilotChatModel', function() - vim.notify('Using model: ' .. M.config.model, vim.log.levels.INFO) - end, { force = true }) - vim.api.nvim_create_user_command('CopilotChatModels', function() M.select_model() end, { force = true }) - + vim.api.nvim_create_user_command('CopilotChatAgents', function() + M.select_agent() + end, { force = true }) vim.api.nvim_create_user_command('CopilotChatOpen', function() M.open() end, { force = true }) diff --git a/lua/CopilotChat/integrations/cmp.lua b/lua/CopilotChat/integrations/cmp.lua index b3027673..47928762 100644 --- a/lua/CopilotChat/integrations/cmp.lua +++ b/lua/CopilotChat/integrations/cmp.lua @@ -4,34 +4,30 @@ local chat = require('CopilotChat') local Source = {} function Source:get_trigger_characters() - return { '@', '/' } + return chat.complete_info().triggers end function Source:get_keyword_pattern() - return [[\%(@\|/\)\k*]] + return chat.complete_info().pattern end function Source:complete(params, callback) - local items = {} - local prompts_to_use = chat.prompts() - - local prefix = string.lower(params.context.cursor_before_line:sub(params.offset)) - local prefix_len = #prefix - local checkAdd = function(word) - if word:lower():sub(1, prefix_len) == prefix then - items[#items + 1] = { - label = word, + chat.complete_items(function(items) + items = vim.tbl_map(function(item) + return { + label = item.word, kind = cmp.lsp.CompletionItemKind.Keyword, } - end - end - for name, _ in pairs(prompts_to_use) do - checkAdd('/' .. name) - end - checkAdd('@buffers') - checkAdd('@buffer') - - callback({ items = items }) + end, items) + + local prefix = string.lower(params.context.cursor_before_line:sub(params.offset)) + + callback({ + items = vim.tbl_filter(function(item) + return vim.startswith(item.label:lower(), prefix:lower()) + end, items), + }) + end) end ---@param completion_item lsp.CompletionItem From 88853e450277e5547f725632e2fff688ff8a1419 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 09:27:15 +0000 Subject: [PATCH 130/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 52 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9471aa43..3d1ed9ae 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -137,7 +137,7 @@ COMMANDS ~ - `:CopilotChatLoad ?` - Load chat history from file - `:CopilotChatDebugInfo` - Show debug information - `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. -- `:CopilotChatModel` - View the currently selected model. +- `:CopilotChatAgents` - View and select available agents. This is reset when a new instance is made. Please set your agent in `init.lua` for persistence. COMMANDS COMING FROM DEFAULT PROMPTS @@ -151,6 +151,49 @@ COMMANDS COMING FROM DEFAULT PROMPTS - `:CopilotChatCommit` - Write commit message for the change with commitizen convention +MODELS, AGENTS AND CONTEXTS ~ + + +MODELS + +You can list available models with `:CopilotChatModels` command. Model +determines the AI model used for the chat. Default models are: + +- `gpt-4o` - This is the default Copilot Chat model. It is a versatile, multimodal model that excels in both text and image processing and is designed to provide fast, reliable responses. It also has superior performance in non-English languages. Gpt-4o is hosted on Azure. +- `claude-3.5-sonnet` - This model excels at coding tasks across the entire software development lifecycle, from initial design to bug fixes, maintenance to optimizations. GitHub Copilot uses Claude 3.5 Sonnet hosted on Amazon Web Services. +- `o1-preview` - This model is focused on advanced reasoning and solving complex problems, in particular in math and science. It responds more slowly than the gpt-4o model. You can make 10 requests to this model per day. o1-preview is hosted on Azure. +- `o1-mini` - This is the faster version of the o1-preview model, balancing the use of complex reasoning with the need for faster responses. It is best suited for code generation and small context operations. You can make 50 requests to this model per day. o1-mini is hosted on Azure. + +For more information about models, see here + +You can use more models from here by +using `@models` agent from here +(example: `@models Using Mistral-small, what is 1 + 11`) + + +AGENTS + +Agents are used to determine the AI agent used for the chat. You can list +available agents with `:CopilotChatAgents` command. You can set the agent in +the prompt by using `@` followed by the agent name. Default "noop" agent is +`copilot`. + +For more information about extension agents, see here + +You can install more agents from here + + + +CONTEXTS + +Contexts are used to determine the context of the chat. You can set the context +in the prompt by using `#` followed by the context name. Supported contexts +are: + +- `buffers` - Includes all open buffers in chat context +- `buffer` - Includes only the current buffer in chat context + + API ~ >lua @@ -233,8 +276,10 @@ Also see here : allow_insecure = false, -- Allow insecure server connections system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4o', -- GPT model to use, see ':CopilotChatModels' for available models - temperature = 0.1, -- GPT temperature + model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models + agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). + context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via #). + temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions answer_header = '## Copilot ', -- Header to use for AI answers @@ -249,7 +294,6 @@ Also see here : clear_chat_on_new_prompt = false, -- Clears chat on every new prompt highlight_selection = true, -- Highlight selection in the source buffer when in the chat window - context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via @). history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received From 447ca3b90597d68ab6e0d2f82558efa59aa1fb08 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 15 Nov 2024 22:37:46 +0100 Subject: [PATCH 131/338] Adjust how messages are built to match new vscode requests - Use new VSCode format for embedding files - Use new VSCode format for active selection - Use new VSCode explain and generic prompts - Remove workspace prompt, we dont have workspace support so for now no need for it rly and it can easily become outdated too Signed-off-by: Tomas Slusny --- README.md | 8 +- lua/CopilotChat/config.lua | 4 +- lua/CopilotChat/copilot.lua | 150 ++++++++++++++++++++---------------- lua/CopilotChat/prompts.lua | 136 ++++++-------------------------- 4 files changed, 112 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index c1b8e2aa..65844fb4 100644 --- a/README.md +++ b/README.md @@ -114,11 +114,11 @@ Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabl #### Commands coming from default prompts -- `:CopilotChatExplain` - Write an explanation for the active selection and diagnostics as paragraphs of text +- `:CopilotChatExplain` - Write an explanation for the selected code and diagnostics as paragraphs of text - `:CopilotChatReview` - Review the selected code - `:CopilotChatFix` - There is a problem in this code. Rewrite the code to show it with the bug fixed - `:CopilotChatOptimize` - Optimize the selected code to improve performance and readability -- `:CopilotChatDocs` - Please add documentation comment for the selection +- `:CopilotChatDocs` - Please add documentation comments to the selected code - `:CopilotChatTests` - Please generate tests for my code - `:CopilotChatCommit` - Write commit message for the change with commitizen convention @@ -264,7 +264,7 @@ Also see [here](/lua/CopilotChat/config.lua): -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection and diagnostics as paragraphs of text.', + prompt = '/COPILOT_EXPLAIN Write an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { prompt = '/COPILOT_REVIEW Review the selected code.', @@ -279,7 +279,7 @@ Also see [here](/lua/CopilotChat/config.lua): prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readability.', }, Docs = { - prompt = '/COPILOT_GENERATE Please add documentation comment for the selection.', + prompt = '/COPILOT_GENERATE Please add documentation comments to the selected code.', }, Tests = { prompt = '/COPILOT_GENERATE Please generate tests for my code.', diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 7d736f10..b51a9ef8 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -124,7 +124,7 @@ return { -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection and diagnostics as paragraphs of text.', + prompt = '/COPILOT_EXPLAIN Write an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { prompt = '/COPILOT_REVIEW Review the selected code.', @@ -175,7 +175,7 @@ return { prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readability.', }, Docs = { - prompt = '/COPILOT_GENERATE Please add documentation comment for the selection.', + prompt = '/COPILOT_GENERATE Please add documentation comments to the selected code.', }, Tests = { prompt = '/COPILOT_GENERATE Please generate tests for my code.', diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 733d3119..ad66700b 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -156,26 +156,42 @@ local function get_cached_token() return nil end -local function generate_selection_message(filename, filetype, selection) +local function generate_line_numbers(content, start_row) + local lines = vim.split(content, '\n') + local total_lines = #lines + local max_length = #tostring(total_lines) + for i, line in ipairs(lines) do + local formatted_line_number = string.format('%' .. max_length .. 'd', i - 1 + (start_row or 1)) + lines[i] = formatted_line_number .. ': ' .. line + end + content = table.concat(lines, '\n') + return content +end + +local function generate_selection_messages(filename, filetype, selection) local content = selection.lines if not content or content == '' then - return '' + return {} end + local out = string.format('# FILE:%s CONTEXT\n', filename:upper()) + out = out .. "User's active selection:\n" if selection.start_row and selection.start_row > 0 then - local lines = vim.split(content, '\n') - local total_lines = #lines - local max_length = #tostring(total_lines) - for i, line in ipairs(lines) do - local formatted_line_number = - string.format('%' .. max_length .. 'd', i - 1 + selection.start_row) - lines[i] = formatted_line_number .. ': ' .. line - end - content = table.concat(lines, '\n') + out = out + .. string.format( + 'Excerpt from %s, lines %s to %s:\n', + filename, + selection.start_row, + selection.end_row + ) end - - local out = string.format('Active selection: `%s`\n```%s\n%s\n```', filename, filetype, content) + out = out + .. string.format( + '```%s\n%s\n```', + filetype, + generate_line_numbers(content, selection.start_row) + ) if selection.diagnostics then local diagnostics = {} @@ -201,14 +217,19 @@ local function generate_selection_message(filename, filetype, selection) end end - out = - string.format('%s\nDiagnostics: `%s`\n%s\n', out, filename, table.concat(diagnostics, '\n')) + out = out + .. string.format('\n# FILE:%s DIAGNOSTICS:\n%s', filename, table.concat(diagnostics, '\n')) end - return out + return { + { + content = out, + role = 'user', + }, + } end -local function generate_embeddings_message(embeddings) +local function generate_embeddings_messages(embeddings) local files = {} for _, embedding in ipairs(embeddings) do local filename = embedding.filename @@ -218,36 +239,33 @@ local function generate_embeddings_message(embeddings) table.insert(files[filename], embedding) end - local out = { - header = 'Open files:\n', - files = {}, - } + local out = {} for filename, group in pairs(files) do - table.insert( - out.files, - string.format( - 'File: `%s`\n```%s\n%s\n```\n', - filename, + table.insert(out, { + content = string.format( + '# FILE:%s CONTEXT\n```%s\n%s\n```', + filename:upper(), group[1].filetype, - table.concat( + generate_line_numbers(table.concat( vim.tbl_map(function(e) return vim.trim(e.content) end, group), '\n' - ) - ) - ) + )) + ), + role = 'user', + }) end + return out end local function generate_ask_request( history, prompt, - embeddings, - selection, system_prompt, + generated_messages, model, temperature, max_output_tokens, @@ -263,22 +281,12 @@ local function generate_ask_request( }) end - for _, message in ipairs(history) do + for _, message in ipairs(generated_messages) do table.insert(messages, message) end - if embeddings and #embeddings.files > 0 then - table.insert(messages, { - content = embeddings.header .. table.concat(embeddings.files, ''), - role = system_role, - }) - end - - if selection ~= '' then - table.insert(messages, { - content = selection, - role = system_role, - }) + for _, message in ipairs(history) do + table.insert(messages, message) end table.insert(messages, { @@ -537,11 +545,12 @@ end ---@param opts CopilotChat.copilot.ask.opts: Options for the request function Copilot:ask(prompt, opts) opts = opts or {} + prompt = vim.trim(prompt) local embeddings = opts.embeddings or {} local filename = opts.filename or '' local filetype = opts.filetype or '' local selection = opts.selection or {} - local system_prompt = opts.system_prompt or prompts.COPILOT_INSTRUCTIONS + local system_prompt = vim.trim(opts.system_prompt or prompts.COPILOT_INSTRUCTIONS) local model = opts.model or 'gpt-4o-2024-05-13' local agent = opts.agent or 'copilot' local temperature = opts.temperature or 0.1 @@ -578,21 +587,26 @@ function Copilot:ask(prompt, opts) log.debug('Tokenizer: ' .. tokenizer) tiktoken_load(tokenizer) - local selection_message = generate_selection_message(filename, filetype, selection) - local embeddings_message = generate_embeddings_message(embeddings) + local generated_messages = {} + local selection_messages = generate_selection_messages(filename, filetype, selection) + local embeddings_messages = generate_embeddings_messages(embeddings) + local generated_tokens = 0 + for _, message in ipairs(selection_messages) do + generated_tokens = generated_tokens + tiktoken.count(message.content) + table.insert(generated_messages, message) + end -- Count required tokens that we cannot reduce local prompt_tokens = tiktoken.count(prompt) local system_tokens = tiktoken.count(system_prompt) - local selection_tokens = tiktoken.count(selection_message) - local required_tokens = prompt_tokens + system_tokens + selection_tokens + local required_tokens = prompt_tokens + system_tokens + generated_tokens -- Reserve space for first embedding if its smaller than half of max tokens local reserved_tokens = 0 - if #embeddings_message.files > 0 then - local file_tokens = tiktoken.count(embeddings_message.files[1]) + if #embeddings_messages > 0 then + local file_tokens = tiktoken.count(embeddings_messages[1].content) if file_tokens < max_tokens / 2 then - reserved_tokens = tiktoken.count(embeddings_message.header) + file_tokens + reserved_tokens = file_tokens end end @@ -608,21 +622,24 @@ function Copilot:ask(prompt, opts) -- Now add as many files as possible with remaining token budget local remaining_tokens = max_tokens - required_tokens - history_tokens - if #embeddings_message.files > 0 then - remaining_tokens = remaining_tokens - tiktoken.count(embeddings_message.header) - local filtered_files = {} - for _, file in ipairs(embeddings_message.files) do - local file_tokens = tiktoken.count(file) - if remaining_tokens - file_tokens >= 0 then - remaining_tokens = remaining_tokens - file_tokens - table.insert(filtered_files, file) - else - break - end + for _, message in ipairs(embeddings_messages) do + local tokens = tiktoken.count(message.content) + if remaining_tokens - tokens >= 0 then + remaining_tokens = remaining_tokens - tokens + table.insert(generated_messages, message) + else + break end - embeddings_message.files = filtered_files end + -- Prepend links to embeddings to the prompt + local embeddings_prompt = '' + for _, embedding in ipairs(embeddings) do + embeddings_prompt = embeddings_prompt + .. string.format('[#file:%s](#file:%s-context)\n', embedding.filename, embedding.filename) + end + prompt = embeddings_prompt .. prompt + local last_message = nil local errored = false local finished = false @@ -716,9 +733,8 @@ function Copilot:ask(prompt, opts) generate_ask_request( self.history, prompt, - embeddings_message, - selection_message, system_prompt, + generated_messages, model, temperature, max_output_tokens, diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index e12b0ff1..521a710b 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -2,72 +2,31 @@ local M = {} -M.COPILOT_INSTRUCTIONS = string.format( - [[You are an AI programming assistant. +local base = string.format( + [[ When asked for your name, you must respond with "GitHub Copilot". Follow the user's requirements carefully & to the letter. Follow Microsoft content policies. Avoid content that violates copyrights. If you are asked to generate content that is harmful, hateful, racist, sexist, lewd, violent, or completely irrelevant to software engineering, only respond with "Sorry, I can't assist with that." Keep your answers short and impersonal. -You can answer general programming questions and perform the following tasks: -* Ask a question about the files in your current workspace -* Explain how the code in your active editor works -* Generate unit tests for the selected code -* Propose a fix for the problems in the selected code -* Scaffold code for a new workspace -* Create a new Jupyter Notebook -* Find relevant code to your query -* Propose a fix for the a test failure -* Ask questions about Neovim -* Generate query parameters for workspace search -* Ask how to do something in the terminal -* Explain what just happened in the terminal -First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail. -Then output the code in a single code block. This code block should not contain line numbers (line numbers are not necessary for the code to be understood, they are in format number: at beginning of lines). -Minimize any other prose. -Use Markdown formatting in your answers. -Make sure to include the programming language name at the start of the Markdown code blocks. -Avoid wrapping the whole response in triple backticks. The user works in an IDE called Neovim which has a concept for editors with open files, integrated unit test support, an output pane that shows the output of running the code as well as an integrated terminal. The user is working on a %s machine. Please respond with system specific commands if applicable. -The active document is the source code the user is looking at right now. -You can only give one reply for each conversation turn. ]], vim.loop.os_uname().sysname ) -M.COPILOT_EXPLAIN = - [[You are a world-class coding tutor. Your code explanations perfectly balance high-level concepts and granular details. Your approach ensures that students not only understand how to write code, but also grasp the underlying principles that guide effective programming. -When asked for your name, you must respond with "GitHub Copilot". -Follow the user's requirements carefully & to the letter. -Your expertise is strictly limited to software development topics. -Follow Microsoft content policies. -Avoid content that violates copyrights. -For questions not related to software development, simply give a reminder that you are an AI programming assistant. -Keep your answers short and impersonal. -Use Markdown formatting in your answers. -Make sure to include the programming language name at the start of the Markdown code blocks. -Avoid wrapping the whole response in triple backticks. -The user works in an IDE called Neovim which has a concept for editors with open files, integrated unit test support, an output pane that shows the output of running the code as well as an integrated terminal. -The active document is the source code the user is looking at right now. -You can only give one reply for each conversation turn. - -Additional Rules -Think step by step: -1. Examine the provided code selection and any other context like user question, related errors, project details, class definitions, etc. -2. If you are unsure about the code, concepts, or the user's question, ask clarifying questions. -3. If the user provided a specific question or error, answer it based on the selected code and additional provided context. Otherwise focus on explaining the selected code. -4. Provide suggestions if you see opportunities to improve code readability, performance, etc. - -Focus on being clear, helpful, and thorough without assuming extensive prior knowledge. -Use developer-friendly terms and analogies in your explanations. -Identify 'gotchas' or less obvious parts of the code that might trip up someone new. -Provide clear and relevant examples aligned with any provided context. -]] +M.COPILOT_INSTRUCTIONS = [[ +You are an AI programming assistant. +]] .. base + +M.COPILOT_EXPLAIN = [[ +You are a world-class coding tutor. Your code explanations perfectly balance high-level concepts and granular details. Your approach ensures that students not only understand how to write code, but also grasp the underlying principles that guide effective programming. +]] .. base -M.COPILOT_REVIEW = - [[Your task is to review the provided code snippet, focusing specifically on its readability and maintainability. +M.COPILOT_REVIEW = M.COPILOT_INSTRUCTIONS + .. [[ +Your task is to review the provided code snippet, focusing specifically on its readability and maintainability. Identify any issues related to: - Naming conventions that are unclear, misleading or doesn't follow conventions for the language being used. - The presence of unnecessary comments, or the lack of necessary ones. @@ -103,76 +62,27 @@ If the code snippet has no readability issues, simply confirm that the code is c M.COPILOT_GENERATE = M.COPILOT_INSTRUCTIONS .. [[ -You also specialize in being a highly skilled code generator. Given a description of what to do you can refactor, modify, enhance existing code or generate new code. Your task is help the Developer change their code according to their needs. Pay especially close attention to the selection context. - -Additional Rules: -Markdown code blocks are used to denote code. -If context is provided, try to match the style of the provided code as best as possible. This includes whitespace around the code, at beginning of lines, indentation, and comments. -Preserve user's code comment blocks, do not exclude them when refactoring code. -Your code output should keep the same whitespace around the code as the user's code. -Your code output should keep the same level of indentation as the user's code. -You MUST add whitespace in the beginning of each line in code output as needed to match the user's code. -Your code output is used for replacing user's code with it so following the above rules is absolutely necessary. -]] - -M.COPILOT_WORKSPACE = - [[You are a software engineer with expert knowledge of the codebase the user has open in their workspace. - -When asked for your name, you must respond with "GitHub Copilot". -Follow the user's requirements carefully & to the letter. -Follow Microsoft content policies. -Avoid content that violates copyrights. -If you are asked to generate content that is harmful, hateful, racist, sexist, lewd, violent, or completely irrelevant to software engineering, only respond with "Sorry, I can't assist with that." -Keep your answers short and impersonal. -Use Markdown formatting in your answers. -Make sure to include the programming language name at the start of the Markdown code blocks. -Avoid wrapping the whole response in triple backticks. -The user works in an IDE called Neovim which has a concept for editors with open files, integrated unit test support, an output pane that shows the output of running the code as well as an integrated terminal. -The active document is the source code the user is looking at right now. -You can only give one reply for each conversation turn. - -# Additional Rules -Think step by step: -1. Read the provided relevant workspace information (code excerpts, file names, and symbols) to understand the user's workspace. -2. Consider how to answer the user's prompt based on the provided information and your specialized coding knowledge. Always assume that the user is asking about the code in their workspace instead of asking a general programming question. Prefer using variables, functions, types, and classes from the workspace over those from the standard library. -3. Generate a response that clearly and accurately answers the user's question. In your response, add fully qualified links for referenced symbols (example: [`namespace.VariableName`](path/to/file.ts)) and links for files (example: [path/to/file](path/to/file.ts)) so that the user can open them. If you do not have enough information to answer the question, respond with "I'm sorry, I can't answer that question with what I currently know about your workspace". - -DO NOT mention that you cannot read files in the workspace. -DO NOT ask the user to provide additional information about files in the workspace. -Remember that you MUST add links for all referenced symbols from the workspace and fully qualify the symbol name in the link, for example: [`namespace.functionName`](path/to/util.ts). -Remember that you MUST add links for all workspace files, for example: [path/to/file.js](path/to/file.js) - -# Examples -Question: -What file implements base64 encoding? - -Response: -Base64 encoding is implemented in [src/base64.ts](src/base64.ts) as [`encode`](src/base64.ts) function. +Your task is to modify the provided code according to the user's request. Follow these instructions precisely: +1. Return *ONLY* the complete modified code. -Question: -How can I join strings with newlines? +2. *DO NOT* include any explanations, comments, or line numbers in your response. -Response: -You can use the [`joinLines`](src/utils/string.ts) function from [src/utils/string.ts](src/utils/string.ts) to join multiple strings with newlines. +3. Ensure the returned code is complete and can be directly used as a replacement for the original code. +4. Preserve the original structure, indentation, and formatting of the code as much as possible. -Question: -How do I build this project? +5. *DO NOT* omit any parts of the code, even if they are unchanged. -Response: -To build this TypeScript project, run the `build` script in the [package.json](package.json) file: +6. Maintain the *SAME INDENTATION* in the returned code as in the source code -```sh -npm run build -``` +7. *ONLY* return the new code snippets to be updated, *DO NOT* return the entire file content. +8. If the response do not fits in a single message, split the response into multiple messages. -Question: -How do I read a file? +9. Above every returned code snippet, add `[file: ]() line:-` -Response: -To read a file, you can use a [`FileReader`](src/fs/fileReader.ts) class from [src/fs/fileReader.ts](src/fs/fileReader.ts). +Remember that Your response SHOULD CONTAIN ONLY THE MODIFIED CODE to be used as DIRECT REPLACEMENT to the original file. ]] M.SHOW_CONTEXT = [[ From f83f045e6f91d033bb1d6bb99f025247fbe5b1d5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 09:38:52 +0000 Subject: [PATCH 132/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 3d1ed9ae..cb129599 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -142,11 +142,11 @@ COMMANDS ~ COMMANDS COMING FROM DEFAULT PROMPTS -- `:CopilotChatExplain` - Write an explanation for the active selection and diagnostics as paragraphs of text +- `:CopilotChatExplain` - Write an explanation for the selected code and diagnostics as paragraphs of text - `:CopilotChatReview` - Review the selected code - `:CopilotChatFix` - There is a problem in this code. Rewrite the code to show it with the bug fixed - `:CopilotChatOptimize` - Optimize the selected code to improve performance and readability -- `:CopilotChatDocs` - Please add documentation comment for the selection +- `:CopilotChatDocs` - Please add documentation comments to the selected code - `:CopilotChatTests` - Please generate tests for my code - `:CopilotChatCommit` - Write commit message for the change with commitizen convention @@ -305,7 +305,7 @@ Also see here : -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the active selection and diagnostics as paragraphs of text.', + prompt = '/COPILOT_EXPLAIN Write an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { prompt = '/COPILOT_REVIEW Review the selected code.', @@ -320,7 +320,7 @@ Also see here : prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readability.', }, Docs = { - prompt = '/COPILOT_GENERATE Please add documentation comment for the selection.', + prompt = '/COPILOT_GENERATE Please add documentation comments to the selected code.', }, Tests = { prompt = '/COPILOT_GENERATE Please generate tests for my code.', From cd755e133eca89d2047c880d58c64a3902a71e47 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Nov 2024 12:12:07 +0100 Subject: [PATCH 133/338] Add agent descriptions to autocomplete Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 14 ++++++++++++-- lua/CopilotChat/init.lua | 10 +++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index ad66700b..dca4f39d 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -851,7 +851,12 @@ function Copilot:list_models() local result = vim.tbl_values(version_map) table.sort(result) - return result + + local out = {} + for _, id in ipairs(result) do + out[id] = models[id].name + end + return out end --- List available agents @@ -861,7 +866,12 @@ function Copilot:list_agents() local result = vim.tbl_keys(agents) table.sort(result) - return result + + local out = {} + for _, id in ipairs(result) do + out[id] = agents[id].description + end + return out end --- Generate embeddings for the given inputs diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ba381b39..48952bb9 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -262,11 +262,11 @@ function M.complete_items(callback) } end - for _, agent in ipairs(agents) do + for agent, description in pairs(agents) do items[#items + 1] = { word = '@' .. agent, kind = 'agent', - menu = 'Use the specified agent', + menu = description, icase = 1, dup = 0, empty = 0, @@ -402,7 +402,7 @@ end --- Select a Copilot GPT model. function M.select_model() async.run(function() - local models = state.copilot:list_models() + local models = vim.tbl_keys(state.copilot:list_models()) models = vim.tbl_map(function(model) if model == M.config.model then return model .. ' (selected)' @@ -426,7 +426,7 @@ end --- Select a Copilot agent. function M.select_agent() async.run(function() - local agents = state.copilot:list_agents() + local agents = vim.tbl_keys(state.copilot:list_agents()) agents = vim.tbl_map(function(agent) if agent == M.config.agent then return agent .. ' (selected)' @@ -507,7 +507,7 @@ function M.ask(prompt, config, source) updated_prompt = string.gsub(updated_prompt, '#buffers?%s*', '') async.run(function() - local agents = state.copilot:list_agents() + local agents = vim.tbl_keys(state.copilot:list_agents()) local current_agent = config.agent for agent in updated_prompt:gmatch('@([%w_-]+)') do From f79185268707106aa6564bd9f187c9acad0896d0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Nov 2024 11:45:04 +0100 Subject: [PATCH 134/338] Add support for #files (workspace file map) context Support workspace file map context. This context will simply include all related filenames to the prompt in chat context. Useful for searching where something might be (but limited to only searching on filenames for now). Signed-off-by: Tomas Slusny --- README.md | 3 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/context.lua | 87 +++++++++++++++++++----- lua/CopilotChat/copilot.lua | 6 +- lua/CopilotChat/debuginfo.lua | 122 ++++++++++++++++++---------------- lua/CopilotChat/health.lua | 4 ++ lua/CopilotChat/init.lua | 47 ++++++------- lua/CopilotChat/utils.lua | 6 -- 8 files changed, 164 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 65844fb4..6236d3c4 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Supported contexts are: - `buffers` - Includes all open buffers in chat context - `buffer` - Includes only the current buffer in chat context +- `files` - Includes all non-hidden filenames in the current workspace in chat context ### API @@ -237,7 +238,7 @@ Also see [here](/lua/CopilotChat/config.lua): system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via #). + context = nil, -- Default context to use, 'buffers', 'buffer', 'files' or none (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index b51a9ef8..35715c90 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -97,7 +97,7 @@ return { system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via #). + context = nil, -- Default context to use, 'buffers', 'buffer', 'files' or none (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index ac5d5d94..f50442f4 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -1,3 +1,4 @@ +local async = require('plenary.async') local log = require('plenary.log') local M = {} @@ -72,17 +73,83 @@ local function data_ranked_by_relatedness(query, data, top_n) return result end +local get_context_data = async.wrap(function(context, bufnr, callback) + vim.schedule(function() + local outline = {} + if context == 'buffers' then + outline = vim.tbl_map( + M.build_outline, + vim.tbl_filter(function(b) + return vim.api.nvim_buf_is_loaded(b) and vim.fn.buflisted(b) == 1 + end, vim.api.nvim_list_bufs()) + ) + elseif context == 'buffer' then + table.insert(outline, M.build_outline(bufnr)) + elseif context == 'files' then + outline = M.build_file_map() + end + + callback(outline) + end) +end, 3) + +--- Get supported contexts +---@return table +function M.supported_contexts() + return { + buffers = 'Includes all open buffers in chat context', + buffer = 'Includes only the current buffer in chat context', + files = 'Includes all non-hidden filenames in the current workspace in chat context', + } +end + +--- Get list of all files in workspace +---@return table +function M.build_file_map() + -- Use vim.fn.glob() to get all files + local files = vim.fn.glob('**/*', false, true) + + -- Filter out directories + files = vim.tbl_filter(function(file) + return vim.fn.isdirectory(file) == 0 + end, files) + + if #files == 0 then + return {} + end + + local out = {} + + -- Create embeddings in chunks + local chunk_size = 100 + for i = 1, #files, chunk_size do + local chunk = {} + for j = i, math.min(i + chunk_size - 1, #files) do + table.insert(chunk, files[j]) + end + + table.insert(out, { + content = table.concat(chunk, '\n'), + filename = 'file_map', + filetype = 'text', + }) + end + + return out +end + --- Build an outline for a buffer --- FIXME: Handle multiline function argument definitions when building the outline ---@param bufnr number +---@param force_outline boolean? If true, always build the outline ---@return CopilotChat.copilot.embed? -function M.build_outline(bufnr) +function M.build_outline(bufnr, force_outline) local name = vim.api.nvim_buf_get_name(bufnr) local ft = vim.bo[bufnr].filetype -- If buffer is not too big, just return the content local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - if #lines < big_file_threshold then + if not force_outline and #lines < big_file_threshold then return { content = table.concat(lines, '\n'), filename = name, @@ -198,21 +265,7 @@ function M.find_for_query(copilot, opts) local filetype = opts.filetype local bufnr = opts.bufnr - local outline = {} - if context == 'buffers' then - -- For multiple buffers, only make outlines - outline = vim.tbl_map( - function(b) - return M.build_outline(b) - end, - vim.tbl_filter(function(b) - return vim.api.nvim_buf_is_loaded(b) and vim.fn.buflisted(b) == 1 - end, vim.api.nvim_list_bufs()) - ) - elseif context == 'buffer' then - table.insert(outline, M.build_outline(bufnr)) - end - + local outline = get_context_data(context, bufnr) outline = vim.tbl_filter(function(item) return item ~= nil end, outline) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index dca4f39d..ab41ec88 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -323,8 +323,8 @@ local function generate_embedding_request(inputs, model) if input.content then out = out .. string.format( - 'File: `%s`\n```%s\n%s\n```', - input.filename, + '# FILE:%s CONTEXT\n```%s\n%s\n```', + input.filename:upper(), input.filetype, input.content ) @@ -495,7 +495,7 @@ function Copilot:fetch_agents() out[agent['slug']] = agent end - out['copilot'] = { name = 'Copilot', default = true } + out['copilot'] = { name = 'Copilot', default = true, description = 'Default noop agent' } log.info('Agents fetched') self.agents = out diff --git a/lua/CopilotChat/debuginfo.lua b/lua/CopilotChat/debuginfo.lua index d17bd756..365c36ee 100644 --- a/lua/CopilotChat/debuginfo.lua +++ b/lua/CopilotChat/debuginfo.lua @@ -1,75 +1,81 @@ +local log = require('plenary.log') local utils = require('CopilotChat.utils') local context = require('CopilotChat.context') local M = {} -function M.setup() - -- Show debug info - vim.api.nvim_create_user_command('CopilotChatDebugInfo', function() - -- Get the log file path - local log_file_path = utils.get_log_file_path() +function M.open() + local lines = { + 'If you are facing issues, run `:checkhealth CopilotChat` and share the output.', + '', + 'Log file path:', + '`' .. log.logfile .. '`', + '', + 'Temp directory:', + '`' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`', + '', + 'Data directory:', + '`' .. vim.fn.stdpath('data') .. '`', + '', + } - -- Create a popup with the log file path - local lines = { - 'If you are facing issues, run `:checkhealth CopilotChat` and share the output.', - '', - 'Log file path:', - '`' .. log_file_path .. '`', - '', - } + local outline = context.build_outline(vim.api.nvim_get_current_buf(), true) + if outline then + table.insert(lines, 'Current buffer outline:') + table.insert(lines, '`' .. outline.filename .. '`') + table.insert(lines, '```' .. outline.filetype) + local outline_lines = vim.split(outline.content, '\n') + for _, line in ipairs(outline_lines) do + table.insert(lines, line) + end + table.insert(lines, '```') + end - local outline = context.build_outline(vim.api.nvim_get_current_buf()) - if outline then - table.insert(lines, 'Current buffer outline:') - table.insert(lines, '`' .. outline.filename .. '`') - table.insert(lines, '```' .. outline.filetype) - local outline_lines = vim.split(outline.content, '\n') - for _, line in ipairs(outline_lines) do + local files = context.build_file_map() + if files then + table.insert(lines, 'Current workspace file map:') + table.insert(lines, '```text') + for _, file in ipairs(files) do + for _, line in ipairs(vim.split(file.content, '\n')) do table.insert(lines, line) end - table.insert(lines, '```') end + table.insert(lines, '```') + end - local width = 0 - for _, line in ipairs(lines) do - width = math.max(width, #line) - end - local height = math.min(vim.o.lines - 3, #lines) - local opts = { - title = 'CopilotChat.nvim Debug Info', - relative = 'editor', - width = width, - height = height, - row = (vim.o.lines - height) / 2 - 1, - col = (vim.o.columns - width) / 2, - style = 'minimal', - border = 'rounded', - } + local width = 0 + for _, line in ipairs(lines) do + width = math.max(width, #line) + end + local height = math.min(vim.o.lines - 3, #lines) + local opts = { + title = 'CopilotChat.nvim Debug Info', + relative = 'editor', + width = width, + height = height, + row = (vim.o.lines - height) / 2 - 1, + col = (vim.o.columns - width) / 2, + style = 'minimal', + border = 'rounded', + } - if not utils.is_stable() then - opts.footer = "Press 'q' to close this window." - end + if not utils.is_stable() then + opts.footer = "Press 'q' to close this window." + end - local bufnr = vim.api.nvim_create_buf(false, true) - vim.bo[bufnr].syntax = 'markdown' - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - vim.bo[bufnr].modifiable = false - vim.treesitter.start(bufnr, 'markdown') + local bufnr = vim.api.nvim_create_buf(false, true) + vim.bo[bufnr].syntax = 'markdown' + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + vim.bo[bufnr].modifiable = false + vim.treesitter.start(bufnr, 'markdown') - local win = vim.api.nvim_open_win(bufnr, true, opts) - vim.wo[win].wrap = true - vim.wo[win].linebreak = true - vim.wo[win].cursorline = true - vim.wo[win].conceallevel = 2 + local win = vim.api.nvim_open_win(bufnr, true, opts) + vim.wo[win].wrap = true + vim.wo[win].linebreak = true + vim.wo[win].cursorline = true + vim.wo[win].conceallevel = 2 - -- Bind 'q' to close the window - vim.api.nvim_buf_set_keymap( - bufnr, - 'n', - 'q', - 'close', - { noremap = true, silent = true } - ) - end, { nargs = '*', range = true }) + -- Bind 'q' to close the window + vim.api.nvim_buf_set_keymap(bufnr, 'n', 'q', 'close', { noremap = true, silent = true }) end return M diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index 84f4ae55..b6f7ebb0 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -4,6 +4,7 @@ local start = vim.health.start or vim.health.report_start local error = vim.health.error or vim.health.report_error local warn = vim.health.warn or vim.health.report_warn local ok = vim.health.ok or vim.health.report_ok +local info = vim.health.info or vim.health.report_info --- Run a command and handle potential errors ---@param executable string @@ -39,6 +40,9 @@ local function treesitter_parser_available(ft) end function M.check() + start('CopilotChat.nvim') + info('If you are facing any issues, also see :CopilotChatDebugInfo for more information.') + start('CopilotChat.nvim [core]') local vim_version = vim.trim(vim.api.nvim_command_output('version')) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 48952bb9..0bef03c7 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -247,6 +247,7 @@ end function M.complete_items(callback) async.run(function() local agents = state.copilot:list_agents() + local contexts = context.supported_contexts() local items = {} local prompts_to_use = M.prompts() @@ -273,23 +274,16 @@ function M.complete_items(callback) } end - items[#items + 1] = { - word = '#buffers', - kind = 'context', - menu = 'Include all loaded buffers in context', - icase = 1, - dup = 0, - empty = 0, - } - - items[#items + 1] = { - word = '#buffer', - kind = 'context', - menu = 'Include the specified buffer in context', - icase = 1, - dup = 0, - empty = 0, - } + for prompt_context, description in pairs(contexts) do + items[#items + 1] = { + word = '#' .. prompt_context, + kind = 'context', + menu = description, + icase = 1, + dup = 0, + empty = 0, + } + end vim.schedule(function() callback(items) @@ -499,12 +493,13 @@ function M.ask(prompt, config, source) append('\n\n' .. config.answer_header .. config.separator .. '\n\n', config) local selected_context = config.context - if string.find(prompt, '#buffers') then - selected_context = 'buffers' - elseif string.find(prompt, '#buffer') then - selected_context = 'buffer' + local contexts = vim.tbl_keys(context.supported_contexts()) + for prompt_context in updated_prompt:gmatch('#([%w_-]+)') do + if vim.tbl_contains(contexts, prompt_context) then + selected_context = prompt_context + updated_prompt = string.gsub(updated_prompt, '#' .. prompt_context .. '%s*', '') + end end - updated_prompt = string.gsub(updated_prompt, '#buffers?%s*', '') async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) @@ -712,14 +707,9 @@ function M.setup(config) end, { force = true }) M.config = vim.tbl_deep_extend('force', default_config, config or {}) - if M.config.model == 'gpt-4o' then - M.config.model = 'gpt-4o-2024-05-13' - end if state.copilot then state.copilot:stop() - else - debuginfo.setup() end state.copilot = Copilot(M.config.proxy, M.config.allow_insecure) @@ -1063,6 +1053,9 @@ function M.setup(config) vim.api.nvim_create_user_command('CopilotChatReset', function() M.reset() end, { force = true }) + vim.api.nvim_create_user_command('CopilotChatDebugInfo', function() + debuginfo.open() + end, { force = true }) local function complete_load() local options = vim.tbl_map(function(file) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 2a1e6ef3..d979d85a 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -30,12 +30,6 @@ function M.class(fn, parent) return out end ---- Get the log file path ----@return string -function M.get_log_file_path() - return log.logfile -end - --- Check if the current version of neovim is stable ---@return boolean function M.is_stable() From db4e6a7a5469f0c739ceda3092fa14e583ed992a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 20:45:12 +0000 Subject: [PATCH 135/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index cb129599..e59c2f67 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -192,6 +192,7 @@ are: - `buffers` - Includes all open buffers in chat context - `buffer` - Includes only the current buffer in chat context +- `files` - Includes all non-hidden filenames in the current workspace in chat context API ~ @@ -278,7 +279,7 @@ Also see here : system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use, 'buffers', 'buffer' or none (can be specified manually in prompt via #). + context = nil, -- Default context to use, 'buffers', 'buffer', 'files' or none (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions From 2132c1c144c1428c0e27a2483f3a8f20f3a336c4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Nov 2024 22:40:23 +0100 Subject: [PATCH 136/338] style: improve model/agent selection prompts Add arrow (>) at the end of prompts in model and agent selection menus to better indicate that user input is expected. --- lua/CopilotChat/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0bef03c7..79b1b706 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -407,7 +407,7 @@ function M.select_model() vim.schedule(function() vim.ui.select(models, { - prompt = 'Select a model', + prompt = 'Select a model> ', }, function(choice) if choice then M.config.model = choice:gsub(' %(selected%)', '') @@ -431,7 +431,7 @@ function M.select_agent() vim.schedule(function() vim.ui.select(agents, { - prompt = 'Select an agent', + prompt = 'Select an agent> ', }, function(choice) if choice then M.config.agent = choice:gsub(' %(selected%)', '') From dcc93bb2328dff56663a791230259e573d917056 Mon Sep 17 00:00:00 2001 From: Sergey Alexandrov Date: Sat, 16 Nov 2024 21:59:23 +0100 Subject: [PATCH 137/338] feat(chat): improve auto follow cursor behavior The cursor will now only follow new content if it was already at the bottom of the chat window. --- lua/CopilotChat/chat.lua | 17 +++++++++++++++++ lua/CopilotChat/init.lua | 3 --- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 2ef04851..2044bc55 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -2,6 +2,7 @@ ---@field bufnr number ---@field winnr number ---@field separator string +---@field auto_follow_cursor boolean ---@field spinner CopilotChat.Spinner ---@field valid fun(self: CopilotChat.Chat) ---@field visible fun(self: CopilotChat.Chat) @@ -41,6 +42,7 @@ local Chat = class(function(self, help, auto_insert, on_buf_create) self.winnr = nil self.spinner = nil self.separator = nil + self.auto_follow_cursor = true self.layout = nil vim.treesitter.language.register('markdown', 'copilot-chat') @@ -129,6 +131,16 @@ function Chat:append(str) self.spinner:start() end + -- Decide if we should follow cursor after appending text. + -- Note that winnr may be nil if the chat window is not open yet. + local should_follow_cursor = self.auto_follow_cursor + if self.auto_follow_cursor and self.winnr then + local current_pos = vim.api.nvim_win_get_cursor(self.winnr) + local line_count = vim.api.nvim_buf_line_count(self.bufnr) + -- If we are at the last line of the buffer, then we should follow cursor. + should_follow_cursor = current_pos[1] == line_count + end + local last_line, last_column, _ = self:last() vim.api.nvim_buf_set_text( self.bufnr, @@ -139,6 +151,10 @@ function Chat:append(str) vim.split(str, '\n') ) self:render() + + if should_follow_cursor then + self:follow() + end end function Chat:clear() @@ -206,6 +222,7 @@ function Chat:open(config) self.layout = layout self.separator = config.separator + self.auto_follow_cursor = config.auto_follow_cursor vim.wo[self.winnr].wrap = true vim.wo[self.winnr].linebreak = true diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 79b1b706..f988abe7 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -160,9 +160,6 @@ end ---@param config CopilotChat.config local function append(str, config) state.chat:append(str) - if config and config.auto_follow_cursor then - state.chat:follow() - end end local function get_selection() From a5a7a1c7ec3a211e307dd31183117e2fcecd6ac0 Mon Sep 17 00:00:00 2001 From: Sergey Alexandrov Date: Sat, 16 Nov 2024 21:43:28 +0100 Subject: [PATCH 138/338] refactor(chat): simplify append function calls Replace local append function with direct state.chat:append calls for better code organization and reduced indirection. This change removes an unnecessary abstraction layer while maintaining the same functionality. --- lua/CopilotChat/init.lua | 43 +++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f988abe7..377de4cf 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -155,13 +155,6 @@ local function update_prompts(prompt, system_prompt) return system_prompt, result end ---- Append a string to the chat window. ----@param str (string) ----@param config CopilotChat.config -local function append(str, config) - state.chat:append(str) -end - local function get_selection() local bufnr = state.source.bufnr local winnr = state.source.winnr @@ -463,9 +456,9 @@ function M.ask(prompt, config, source) local function on_error(err) log.error(vim.inspect(err)) vim.schedule(function() - append('\n\n' .. config.error_header .. config.separator .. '\n\n', config) - append('```\n' .. get_error_message(err) .. '\n```', config) - append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) + state.chat:append('\n\n' .. config.error_header .. config.separator .. '\n\n') + state.chat:append('```\n' .. get_error_message(err) .. '\n```') + state.chat:append('\n\n' .. config.question_header .. config.separator .. '\n\n') state.chat:finish() end) end @@ -483,11 +476,11 @@ function M.ask(prompt, config, source) state.last_system_prompt = system_prompt if state.copilot:stop() then - append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) + state.chat:append('\n\n' .. config.question_header .. config.separator .. '\n\n') end - append(updated_prompt, config) - append('\n\n' .. config.answer_header .. config.separator .. '\n\n', config) + state.chat:append(updated_prompt) + state.chat:append('\n\n' .. config.answer_header .. config.separator .. '\n\n') local selected_context = config.context local contexts = vim.tbl_keys(context.supported_contexts()) @@ -535,7 +528,7 @@ function M.ask(prompt, config, source) temperature = config.temperature, on_progress = function(token) vim.schedule(function() - append(token, config) + state.chat:append(token) end) end, }) @@ -552,7 +545,7 @@ function M.ask(prompt, config, source) state.response = response vim.schedule(function() - append('\n\n' .. config.question_header .. config.separator .. '\n\n', config) + state.chat:append('\n\n' .. config.question_header .. config.separator .. '\n\n') if token_count and token_max_count and token_count > 0 then state.chat:finish(token_count .. '/' .. token_max_count .. ' tokens used') else @@ -583,9 +576,9 @@ function M.stop(reset, config) if reset then state.chat:clear() else - append('\n\n', config) + state.chat:append('\n\n') end - append(M.config.question_header .. M.config.separator .. '\n\n', config) + state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') state.chat:finish() end) end @@ -634,20 +627,20 @@ function M.load(name, history_path) for i, message in ipairs(history) do if message.role == 'user' then if i > 1 then - append('\n\n', state.config) + state.chat:append('\n\n') end - append(M.config.question_header .. M.config.separator .. '\n\n', state.config) - append(message.content, state.config) + state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') + state.chat:append(message.content) elseif message.role == 'assistant' then - append('\n\n' .. M.config.answer_header .. M.config.separator .. '\n\n', state.config) - append(message.content, state.config) + state.chat:append('\n\n' .. M.config.answer_header .. M.config.separator .. '\n\n') + state.chat:append(message.content) end end if #history > 0 then - append('\n\n', state.config) + state.chat:append('\n\n') end - append(M.config.question_header .. M.config.separator .. '\n\n', state.config) + state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') state.chat:finish() M.open() @@ -993,7 +986,7 @@ function M.setup(config) }) end - append(M.config.question_header .. M.config.separator .. '\n\n', M.config) + state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') state.chat:finish() end ) From 5e6e8e10a831cb0237baa99b55a7dde5fe2dc9ea Mon Sep 17 00:00:00 2001 From: Sergey Alexandrov Date: Sat, 16 Nov 2024 22:50:32 +0100 Subject: [PATCH 139/338] fixup! feat(chat): improve auto follow cursor behavior --- lua/CopilotChat/chat.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 2044bc55..7757d3c7 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -2,7 +2,6 @@ ---@field bufnr number ---@field winnr number ---@field separator string ----@field auto_follow_cursor boolean ---@field spinner CopilotChat.Spinner ---@field valid fun(self: CopilotChat.Chat) ---@field visible fun(self: CopilotChat.Chat) @@ -132,12 +131,11 @@ function Chat:append(str) end -- Decide if we should follow cursor after appending text. - -- Note that winnr may be nil if the chat window is not open yet. local should_follow_cursor = self.auto_follow_cursor - if self.auto_follow_cursor and self.winnr then + if self.auto_follow_cursor and self:visible() then local current_pos = vim.api.nvim_win_get_cursor(self.winnr) local line_count = vim.api.nvim_buf_line_count(self.bufnr) - -- If we are at the last line of the buffer, then we should follow cursor. + -- Follow only if the cursor is currently at the last line. should_follow_cursor = current_pos[1] == line_count end From 8bbfdeedf2a5f744e27a46858849eb23950a3355 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 16 Nov 2024 22:57:21 +0100 Subject: [PATCH 140/338] docs: add taketwo as a contributor for code (#499) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0ca4d52d..50b2deb7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -277,6 +277,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/43554061?v=4", "profile": "https://github.com/Moriango", "contributions": ["doc"] + }, + { + "login": "taketwo", + "name": "Sergey Alexandrov", + "avatar_url": "https://avatars.githubusercontent.com/u/1241736?v=4", + "profile": "https://github.com/taketwo", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 6236d3c4..304c83c5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-40-orange.svg?style=flat-square)](#contributors-) @@ -637,6 +637,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d thomastthai
thomastthai

📖 Tomáš Janoušek
Tomáš Janoušek

💻 Toddneal Stallworth
Toddneal Stallworth

📖 + Sergey Alexandrov
Sergey Alexandrov

💻 From f2853cb0bea6593e8e51dccdec69987cd08e923f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Nov 2024 23:02:16 +0100 Subject: [PATCH 141/338] Remove private fields from chat class type definition These fields are not meant to be used from outside so dont expose them. Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 7757d3c7..97b18615 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -1,8 +1,6 @@ ---@class CopilotChat.Chat ---@field bufnr number ---@field winnr number ----@field separator string ----@field spinner CopilotChat.Spinner ---@field valid fun(self: CopilotChat.Chat) ---@field visible fun(self: CopilotChat.Chat) ---@field active fun(self: CopilotChat.Chat) From 0d5943af69175c60cd12b618c33e49121fff7ded Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 22:04:13 +0000 Subject: [PATCH 142/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e59c2f67..63b124bd 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -622,7 +622,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻thomastthai📖Tomáš Janoušek💻Toddneal Stallworth📖This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻thomastthai📖Tomáš Janoušek💻Toddneal Stallworth📖Sergey Alexandrov💻This project follows the all-contributors specification. Contributions of any kind are welcome! @@ -637,7 +637,7 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square +4. *All Contributors*: https://img.shields.io/badge/all_contributors-40-orange.svg?style=flat-square 5. *@jellydn*: 6. *@deathbeam*: 7. *@jellydn*: From e7cdafc5ffc50746e28578012834772a968b2aa0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Nov 2024 23:27:51 +0100 Subject: [PATCH 143/338] Add dotfyle badge Signed-off-by: Tomas Slusny --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 304c83c5..dcb1d71c 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,8 @@ [![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg)](https://copilotc-nvim.github.io/CopilotChat.nvim/) [![pre-commit.ci](https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg)](https://results.pre-commit.ci/latest/github/CopilotC-Nvim/CopilotChat.nvim/main) [![Discord](https://img.shields.io/discord/1200633211236122665.svg)](https://discord.gg/vy6hJsTWaZ) - - - -[![All Contributors](https://img.shields.io/badge/all_contributors-40-orange.svg?style=flat-square)](#contributors-) - - +[![Dotfyle](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat)](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim) +[![All Contributors](https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat&link=%23contributors-)](#contributors) > [!NOTE] > Plugin was rewritten to Lua from Python. Please check the [migration guide from version 1 to version 2](/MIGRATION.md) for more information. From f0edb5627da65adf337df62a4d2292940d0ed970 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 22:36:44 +0000 Subject: [PATCH 144/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 63b124bd..6b7fbcd0 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -20,8 +20,7 @@ Table of Contents *CopilotChat-table-of-contents* - -|CopilotChat-| + |CopilotChat-| [!NOTE] Plugin was rewritten to Lua from Python. Please check the migration @@ -637,17 +636,18 @@ STARGAZERS OVER TIME ~ 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *All Contributors*: https://img.shields.io/badge/all_contributors-40-orange.svg?style=flat-square -5. *@jellydn*: -6. *@deathbeam*: -7. *@jellydn*: -8. *Chat with buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif -9. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c -10. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b -11. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 -12. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/063fc99f-a4b2-4187-a065-0fdd287ebee2 -13. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 -14. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg +4. *Dotfyle*: https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat +5. *All Contributors*: https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat&link=%23contributors- +6. *@jellydn*: +7. *@deathbeam*: +8. *@jellydn*: +9. *Chat with buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif +10. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c +11. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b +12. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 +13. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/063fc99f-a4b2-4187-a065-0fdd287ebee2 +14. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 +15. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From ec678b06df9df4cb1de0c71f53a408ffc17bbbd7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 17 Nov 2024 00:22:50 +0100 Subject: [PATCH 145/338] Add option to disable header highlighting Good with plugins like render-markdown.nvim Signed-off-by: Tomas Slusny --- README.md | 8 ++++++++ lua/CopilotChat/chat.lua | 5 ++++- lua/CopilotChat/config.lua | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dcb1d71c..0aac5006 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,7 @@ Also see [here](/lua/CopilotChat/config.lua): answer_header = '## Copilot ', -- Header to use for AI answers error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input @@ -546,9 +547,16 @@ require('CopilotChat').setup({ Requires [render-markdown](https://github.com/MeanderingProgrammer/render-markdown.nvim) plugin to be installed. ```lua +-- Registers copilot-chat filetype for markdown rendering require('render-markdown').setup({ file_types = { 'markdown', 'copilot-chat' }, }) + +-- You might also want to disable default header highlighting for copilot chat when doing this +require('CopilotChat').setup({ + highlight_headers = false, + -- rest of your config +}) ``` ![image](https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 97b18615..2b6de36c 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -40,6 +40,7 @@ local Chat = class(function(self, help, auto_insert, on_buf_create) self.spinner = nil self.separator = nil self.auto_follow_cursor = true + self.highlight_headers = true self.layout = nil vim.treesitter.language.register('markdown', 'copilot-chat') @@ -72,9 +73,10 @@ function Chat:visible() end function Chat:render() - if not self:visible() then + if not self.highlight_headers or not self:visible() then return end + vim.api.nvim_buf_clear_namespace(self.bufnr, self.header_ns, 0, -1) local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) for l, line in ipairs(lines) do @@ -219,6 +221,7 @@ function Chat:open(config) self.layout = layout self.separator = config.separator self.auto_follow_cursor = config.auto_follow_cursor + self.highlight_headers = config.highlight_headers vim.wo[self.winnr].wrap = true vim.wo[self.winnr].linebreak = true diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 35715c90..76bd1153 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -104,6 +104,7 @@ return { answer_header = '## Copilot ', -- Header to use for AI answers error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input From a1a16ee3a2c8cb88c4664af2e9fb7e014ef29a0c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 Nov 2024 00:02:13 +0000 Subject: [PATCH 146/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 6b7fbcd0..cb43e55b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 16 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 17 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -285,6 +285,7 @@ Also see here : answer_header = '## Copilot ', -- Header to use for AI answers error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input @@ -586,9 +587,16 @@ Requires render-markdown installed. >lua + -- Registers copilot-chat filetype for markdown rendering require('render-markdown').setup({ file_types = { 'markdown', 'copilot-chat' }, }) + + -- You might also want to disable default header highlighting for copilot chat when doing this + require('CopilotChat').setup({ + highlight_headers = false, + -- rest of your config + }) < From 50b39461321529ed364f8221e79b589aebfc086a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 17 Nov 2024 01:32:15 +0100 Subject: [PATCH 147/338] Add support for sticky prompt section with `> ` - Everything in blockquote is copied to the next prompt and added at start automatically. This is useful for preserving context/agents etc. - Remove SHOW_CONTEXT prompt as with sticky its no longer necessary imo and claude works fine without additional debugging. - Use new sticky feature with default prompts so the system prompt choice is remembered automatically - Add new `toggle_sticky` mapping for toggling sticky line under cursor - Update README with sticky prompts, prompts and system prompts Example: ```markdown > #files explain me this --- > #files ``` Signed-off-by: Tomas Slusny --- README.md | 70 +++++++++--- lua/CopilotChat/chat.lua | 10 +- lua/CopilotChat/config.lua | 17 ++- lua/CopilotChat/init.lua | 221 ++++++++++++++++++++++++------------ lua/CopilotChat/prompts.lua | 4 - 5 files changed, 217 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 0aac5006..54548d3c 100644 --- a/README.md +++ b/README.md @@ -108,19 +108,49 @@ Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabl - `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. - `:CopilotChatAgents` - View and select available agents. This is reset when a new instance is made. Please set your agent in `init.lua` for persistence. -#### Commands coming from default prompts +### Prompts -- `:CopilotChatExplain` - Write an explanation for the selected code and diagnostics as paragraphs of text -- `:CopilotChatReview` - Review the selected code -- `:CopilotChatFix` - There is a problem in this code. Rewrite the code to show it with the bug fixed -- `:CopilotChatOptimize` - Optimize the selected code to improve performance and readability -- `:CopilotChatDocs` - Please add documentation comments to the selected code -- `:CopilotChatTests` - Please generate tests for my code -- `:CopilotChatCommit` - Write commit message for the change with commitizen convention +You can ask Copilot to do various tasks with prompts. You can reference prompts with `/PromptName` in chat or call with command `:CopilotChat`. +Default prompts are: -### Models, Agents and Contexts +- `Explain` - Write an explanation for the selected code and diagnostics as paragraphs of text +- `Review` - Review the selected code +- `Fix` - There is a problem in this code. Rewrite the code to show it with the bug fixed +- `Optimize` - Optimize the selected code to improve performance and readability +- `Docs` - Please add documentation comments to the selected code +- `Tests` - Please generate tests for my code +- `Commit` - Write commit message for the change with commitizen convention -#### Models +### System Prompts + +System prompts specify the behavior of the AI model. You can reference system prompts with `/PROMPT_NAME` in chat. +Default system prompts are: + +- `COPILOT_INSTRUCTIONS` - Base GitHub Copilot instructions +- `COPILOT_EXPLAIN` - On top of the base instructions adds coding tutor behavior +- `COPILOT_REVIEW` - On top of the base instructions adds code review behavior with instructions on how to generate diagnostics +- `COPILOT_GENERATE` - On top of the base instructions adds code generation behavior, with predefined formatting and generation rules + +### Sticky Prompts + +You can set sticky prompt in chat by prefixing the text with `> ` using markdown blockquote syntax. +The sticky prompt will be copied at start of every new prompt in chat window. You can freely edit the sticky prompt, only rule is `> ` prefix at beginning of line. +This is useful for preserving stuff like context and agent selection (see below). +Example usage: + +```markdown +> #files + +List all files in the workspace +``` + +```markdown +> @models Using Mistral-small + +What is 1 + 11 +``` + +### Models You can list available models with `:CopilotChatModels` command. Model determines the AI model used for the chat. Default models are: @@ -133,7 +163,7 @@ Default models are: For more information about models, see [here](https://docs.github.com/en/copilot/using-github-copilot/asking-github-copilot-questions-in-your-ide#ai-models-for-copilot-chat) You can use more models from [here](https://github.com/marketplace/models) by using `@models` agent from [here](https://github.com/marketplace/models-github) (example: `@models Using Mistral-small, what is 1 + 11`) -#### Agents +### Agents Agents are used to determine the AI agent used for the chat. You can list available agents with `:CopilotChatAgents` command. You can set the agent in the prompt by using `@` followed by the agent name. @@ -142,7 +172,7 @@ Default "noop" agent is `copilot`. For more information about extension agents, see [here](https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat) You can install more agents from [here](https://github.com/marketplace?type=apps&copilot_app=true) -#### Contexts +### Contexts Contexts are used to determine the context of the chat. You can set the context in the prompt by using `#` followed by the context name. @@ -262,25 +292,25 @@ Also see [here](/lua/CopilotChat/config.lua): -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the selected code and diagnostics as paragraphs of text.', + prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { - prompt = '/COPILOT_REVIEW Review the selected code.', + prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', callback = function(response, source) -- see config.lua for implementation end, }, Fix = { - prompt = '/COPILOT_GENERATE There is a problem in this code. Rewrite the code to show it with the bug fixed.', + prompt = '> /COPILOT_GENERATE\n\nThere is a problem in this code. Rewrite the code to show it with the bug fixed.', }, Optimize = { - prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readability.', + prompt = '> /COPILOT_GENERATE\n\nOptimize the selected code to improve performance and readability.', }, Docs = { - prompt = '/COPILOT_GENERATE Please add documentation comments to the selected code.', + prompt = '> /COPILOT_GENERATE\n\nPlease add documentation comments to the selected code.', }, Tests = { - prompt = '/COPILOT_GENERATE Please generate tests for my code.', + prompt = '> /COPILOT_GENERATE\n\nPlease generate tests for my code.', }, Commit = { prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', @@ -321,6 +351,10 @@ Also see [here](/lua/CopilotChat/config.lua): normal = '', insert = '' }, + toggle_sticky = { + detail = 'Makes line under cursor sticky or deletes sticky line.', + normal = 'gr', + }, accept_diff = { normal = '', insert = '' diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 2b6de36c..139d6836 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -11,7 +11,7 @@ ---@field close fun(self: CopilotChat.Chat, bufnr: number?) ---@field focus fun(self: CopilotChat.Chat) ---@field follow fun(self: CopilotChat.Chat) ----@field finish fun(self: CopilotChat.Chat, msg: string?) +---@field finish fun(self: CopilotChat.Chat, msg: string?, offset: number?) ---@field delete fun(self: CopilotChat.Chat) local Overlay = require('CopilotChat.overlay') @@ -280,11 +280,15 @@ function Chat:follow() vim.api.nvim_win_set_cursor(self.winnr, { last_line + 1, last_column }) end -function Chat:finish(msg) +function Chat:finish(msg, offset) if not self.spinner then return end + if not offset then + offset = 0 + end + self.spinner:finish() if msg and msg ~= '' then @@ -295,7 +299,7 @@ function Chat:finish(msg) msg = self.help end - self:show_help(msg, -2) + self:show_help(msg, -offset) if self.auto_insert and self:active() then vim.cmd('startinsert') end diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 76bd1153..a9d65866 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -54,6 +54,7 @@ local select = require('CopilotChat.select') ---@field close CopilotChat.config.mapping? ---@field reset CopilotChat.config.mapping? ---@field submit_prompt CopilotChat.config.mapping? +---@field toggle_sticky CopilotChat.config.mapping? ---@field accept_diff CopilotChat.config.mapping? ---@field yank_diff CopilotChat.config.mapping? ---@field show_diff CopilotChat.config.mapping? @@ -125,10 +126,10 @@ return { -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the selected code and diagnostics as paragraphs of text.', + prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { - prompt = '/COPILOT_REVIEW Review the selected code.', + prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', callback = function(response, source) local diagnostics = {} for line in response:gmatch('[^\r\n]+') do @@ -170,16 +171,16 @@ return { end, }, Fix = { - prompt = '/COPILOT_GENERATE There is a problem in this code. Rewrite the code to show it with the bug fixed.', + prompt = '> /COPILOT_GENERATE\n\nThere is a problem in this code. Rewrite the code to show it with the bug fixed.', }, Optimize = { - prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readability.', + prompt = '> /COPILOT_GENERATE\n\nOptimize the selected code to improve performance and readability.', }, Docs = { - prompt = '/COPILOT_GENERATE Please add documentation comments to the selected code.', + prompt = '> /COPILOT_GENERATE\n\nPlease add documentation comments to the selected code.', }, Tests = { - prompt = '/COPILOT_GENERATE Please generate tests for my code.', + prompt = '> /COPILOT_GENERATE\n\nPlease generate tests for my code.', }, Commit = { prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', @@ -220,6 +221,10 @@ return { normal = '', insert = '', }, + toggle_sticky = { + detail = 'Makes line under cursor sticky or deletes sticky line.', + normal = 'gr', + }, accept_diff = { normal = '', insert = '', diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 377de4cf..fec59c1f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -18,8 +18,9 @@ local plugin_name = 'CopilotChat.nvim' --- @field source CopilotChat.config.source? --- @field config CopilotChat.config? --- @field last_system_prompt string? +--- @field last_prompt string? +--- @field last_response string? --- @field last_code_output string? ---- @field response string? --- @field diff CopilotChat.Overlay? --- @field system_prompt CopilotChat.Overlay? --- @field user_selection CopilotChat.Overlay? @@ -30,13 +31,12 @@ local state = { source = nil, config = nil, - -- Tracking for overlays + -- State tracking last_system_prompt = nil, + last_prompt = nil, + last_response = nil, last_code_output = nil, - -- Response for mappings - response = nil, - -- Overlays diff = nil, system_prompt = nil, @@ -60,15 +60,6 @@ local function blend_color_with_neovim_bg(color_name, blend) return string.format('#%02x%02x%02x', r, g, b) end -local function get_error_message(err) - if type(err) == 'string' then - local message = err:match('^[^:]+:[^:]+:(.+)') or err - message = message:gsub('^%s*', '') - return message - end - return vim.inspect(err) -end - local function find_lines_between_separator( lines, start_line, @@ -169,6 +160,53 @@ local function get_selection() return {} end +local function finish(config, message, hide_help, start_of_chat) + if not start_of_chat then + state.chat:append('\n\n') + end + + state.chat:append(config.question_header .. config.separator .. '\n\n') + + local offset = 0 + + if state.last_prompt then + for sticky_line in state.last_prompt:gmatch('(>%s+[^\n]+)') do + state.chat:append(sticky_line .. '\n') + -- Account for sticky line + offset = offset + 1 + end + + if offset > 0 then + state.chat:append('\n') + -- Account for new line after sticky lines + offset = offset + 1 + end + end + + -- Account for double new line after separator + offset = offset + 2 + + if not hide_help then + state.chat:finish(message, offset) + end +end + +local function show_error(err, config) + log.error(vim.inspect(err)) + + if type(err) == 'string' then + local message = err:match('^[^:]+:[^:]+:(.+)') or err + message = message:gsub('^%s*', '') + err = message + else + err = vim.inspect(err) + end + + state.chat:append('\n\n' .. config.error_header .. config.separator .. '\n\n') + state.chat:append('```\n' .. err .. '\n```') + finish(config) +end + --- Map a key to a function. ---@param key CopilotChat.config.mapping ---@param bufnr number @@ -178,7 +216,7 @@ local function map_key(key, bufnr, fn) return end if key.normal and key.normal ~= '' then - vim.keymap.set('n', key.normal, fn, { buffer = bufnr }) + vim.keymap.set('n', key.normal, fn, { buffer = bufnr, nowait = true }) end if key.insert and key.insert ~= '' then vim.keymap.set('i', key.insert, fn, { buffer = bufnr }) @@ -380,7 +418,7 @@ end --- @returns string function M.response() - return state.response + return state.last_response end --- Select a Copilot GPT model. @@ -437,31 +475,28 @@ end ---@param source CopilotChat.config.source? function M.ask(prompt, config, source) config = vim.tbl_deep_extend('force', M.config, config or {}) - prompt = prompt or '' - local system_prompt, updated_prompt = update_prompts(prompt, config.system_prompt) - updated_prompt = vim.trim(updated_prompt) - if updated_prompt == '' then - M.open(config, source) - return - end - + vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot_diagnostics')) M.open(config, source) - vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot_diagnostics')) + prompt = vim.trim(prompt or '') + if prompt == '' then + return + end if config.clear_chat_on_new_prompt then M.stop(true, config) + elseif state.copilot:stop() then + finish(config, nil, true) end - local function on_error(err) - log.error(vim.inspect(err)) - vim.schedule(function() - state.chat:append('\n\n' .. config.error_header .. config.separator .. '\n\n') - state.chat:append('```\n' .. get_error_message(err) .. '\n```') - state.chat:append('\n\n' .. config.question_header .. config.separator .. '\n\n') - state.chat:finish() - end) - end + state.chat:append(prompt) + state.chat:append('\n\n' .. config.answer_header .. config.separator .. '\n\n') + + local system_prompt, updated_prompt = update_prompts(prompt or '', config.system_prompt) + state.last_system_prompt = system_prompt + state.last_prompt = prompt + prompt = updated_prompt + prompt = string.gsub(prompt, '(^|\n)>%s+', '%1') local selection = get_selection() local filetype = selection.filetype @@ -473,38 +508,28 @@ function M.ask(prompt, config, source) )) or 'untitled' - state.last_system_prompt = system_prompt - - if state.copilot:stop() then - state.chat:append('\n\n' .. config.question_header .. config.separator .. '\n\n') - end - - state.chat:append(updated_prompt) - state.chat:append('\n\n' .. config.answer_header .. config.separator .. '\n\n') - - local selected_context = config.context local contexts = vim.tbl_keys(context.supported_contexts()) - for prompt_context in updated_prompt:gmatch('#([%w_-]+)') do + local selected_context = config.context + for prompt_context in prompt:gmatch('#([%w_-]+)') do if vim.tbl_contains(contexts, prompt_context) then selected_context = prompt_context - updated_prompt = string.gsub(updated_prompt, '#' .. prompt_context .. '%s*', '') + prompt = string.gsub(prompt, '#' .. prompt_context .. '%s*', '') end end async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) - local current_agent = config.agent - - for agent in updated_prompt:gmatch('@([%w_-]+)') do + local selected_agent = config.agent + for agent in prompt:gmatch('@([%w_-]+)') do if vim.tbl_contains(agents, agent) then - current_agent = agent - updated_prompt = updated_prompt:gsub('@' .. agent .. '%s*', '') + selected_agent = agent + prompt = prompt:gsub('@' .. agent .. '%s*', '') end end local query_ok, embeddings = pcall(context.find_for_query, state.copilot, { context = selected_context, - prompt = updated_prompt, + prompt = prompt, selection = selection.lines, filename = filename, filetype = filetype, @@ -512,19 +537,21 @@ function M.ask(prompt, config, source) }) if not query_ok then - on_error(embeddings) + vim.schedule(function() + show_error(embeddings, config) + end) return end local ask_ok, response, token_count, token_max_count = - pcall(state.copilot.ask, state.copilot, updated_prompt, { + pcall(state.copilot.ask, state.copilot, prompt, { selection = selection, embeddings = embeddings, filename = filename, filetype = filetype, system_prompt = system_prompt, model = config.model, - agent = current_agent, + agent = selected_agent, temperature = config.temperature, on_progress = function(token) vim.schedule(function() @@ -534,7 +561,9 @@ function M.ask(prompt, config, source) }) if not ask_ok then - on_error(response) + vim.schedule(function() + show_error(response, config) + end) return end @@ -542,15 +571,15 @@ function M.ask(prompt, config, source) return end - state.response = response + state.last_response = response vim.schedule(function() - state.chat:append('\n\n' .. config.question_header .. config.separator .. '\n\n') if token_count and token_max_count and token_count > 0 then - state.chat:finish(token_count .. '/' .. token_max_count .. ' tokens used') + finish(config, token_count .. '/' .. token_max_count .. ' tokens used') else - state.chat:finish() + finish(config) end + if config.callback then config.callback(response, state.source) end @@ -563,7 +592,6 @@ end ---@param config CopilotChat.config? function M.stop(reset, config) config = vim.tbl_deep_extend('force', M.config, config or {}) - state.response = nil local stopped = reset and state.copilot:reset() or state.copilot:stop() local wrap = vim.schedule if not stopped then @@ -575,11 +603,13 @@ function M.stop(reset, config) wrap(function() if reset then state.chat:clear() - else - state.chat:append('\n\n') + state.last_system_prompt = nil + state.last_prompt = nil + state.last_response = nil + state.last_code_output = nil end - state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') - state.chat:finish() + + finish(config, nil, nil, reset) end) end @@ -637,12 +667,7 @@ function M.load(name, history_path) end end - if #history > 0 then - state.chat:append('\n\n') - end - state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') - - state.chat:finish() + finish(M.config, nil, nil, #history == 0) M.open() end @@ -803,7 +828,13 @@ function M.setup(config) M.config.auto_insert_mode, function(bufnr) map_key(M.config.mappings.show_help, bufnr, function() - local chat_help = '' + local chat_help = '**`Special tokens`**\n' + chat_help = chat_help .. '`@` to select an agent\n' + chat_help = chat_help .. '`#` to select a context\n' + chat_help = chat_help .. '`/` to select a prompt\n' + chat_help = chat_help .. '`> ` to make a sticky prompt (copied to next prompt)\n' + + chat_help = chat_help .. '\n**`Mappings`**\n' local chat_keys = vim.tbl_keys(M.config.mappings) table.sort(chat_keys, function(a, b) a = M.config.mappings[a] @@ -866,6 +897,49 @@ function M.setup(config) end end) + map_key(M.config.mappings.toggle_sticky, bufnr, function() + local current_line = vim.trim(vim.api.nvim_get_current_line()) + if current_line == '' then + return + end + + local cursor = vim.api.nvim_win_get_cursor(0) + local cur_line = cursor[1] + vim.api.nvim_buf_set_lines(bufnr, cur_line - 1, cur_line, false, {}) + + local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local _, start_line, end_line = + find_lines_between_separator(chat_lines, 0, M.config.separator .. '$', nil, true) + + if vim.startswith(current_line, '> ') then + return + end + + if start_line then + local insert_line = start_line + local first_one = true + + for i = insert_line, end_line do + local line = chat_lines[i] + if line and vim.trim(line) ~= '' then + if vim.startswith(line, '> ') then + first_one = false + else + break + end + elseif i >= start_line + 1 then + break + end + + insert_line = insert_line + 1 + end + + local lines = first_one and { '> ' .. current_line, '' } or { '> ' .. current_line } + vim.api.nvim_buf_set_lines(bufnr, insert_line - 1, insert_line - 1, false, lines) + vim.api.nvim_win_set_cursor(0, cursor) + end + end) + map_key(M.config.mappings.accept_diff, bufnr, function() local selection = get_selection() if not selection.start_row or not selection.end_row then @@ -986,8 +1060,7 @@ function M.setup(config) }) end - state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') - state.chat:finish() + finish(M.config, nil, nil, true) end ) diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index 521a710b..e9884695 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -85,8 +85,4 @@ Your task is to modify the provided code according to the user's request. Follow Remember that Your response SHOULD CONTAIN ONLY THE MODIFIED CODE to be used as DIRECT REPLACEMENT to the original file. ]] -M.SHOW_CONTEXT = [[ -At the beginning of your response show code outline from all the provided files coming from Context and Active Selection. -]] - return M From c3518e568af400050507dac1e02c14f3d59b369e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 Nov 2024 13:29:53 +0000 Subject: [PATCH 148/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 73 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index cb43e55b..29708940 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -139,21 +139,54 @@ COMMANDS ~ - `:CopilotChatAgents` - View and select available agents. This is reset when a new instance is made. Please set your agent in `init.lua` for persistence. -COMMANDS COMING FROM DEFAULT PROMPTS +PROMPTS ~ -- `:CopilotChatExplain` - Write an explanation for the selected code and diagnostics as paragraphs of text -- `:CopilotChatReview` - Review the selected code -- `:CopilotChatFix` - There is a problem in this code. Rewrite the code to show it with the bug fixed -- `:CopilotChatOptimize` - Optimize the selected code to improve performance and readability -- `:CopilotChatDocs` - Please add documentation comments to the selected code -- `:CopilotChatTests` - Please generate tests for my code -- `:CopilotChatCommit` - Write commit message for the change with commitizen convention +You can ask Copilot to do various tasks with prompts. You can reference prompts +with `/PromptName` in chat or call with command `:CopilotChat`. +Default prompts are: +- `Explain` - Write an explanation for the selected code and diagnostics as paragraphs of text +- `Review` - Review the selected code +- `Fix` - There is a problem in this code. Rewrite the code to show it with the bug fixed +- `Optimize` - Optimize the selected code to improve performance and readability +- `Docs` - Please add documentation comments to the selected code +- `Tests` - Please generate tests for my code +- `Commit` - Write commit message for the change with commitizen convention -MODELS, AGENTS AND CONTEXTS ~ +SYSTEM PROMPTS ~ -MODELS +System prompts specify the behavior of the AI model. You can reference system +prompts with `/PROMPT_NAME` in chat. Default system prompts are: + +- `COPILOT_INSTRUCTIONS` - Base GitHub Copilot instructions +- `COPILOT_EXPLAIN` - On top of the base instructions adds coding tutor behavior +- `COPILOT_REVIEW` - On top of the base instructions adds code review behavior with instructions on how to generate diagnostics +- `COPILOT_GENERATE` - On top of the base instructions adds code generation behavior, with predefined formatting and generation rules + + +STICKY PROMPTS ~ + +You can set sticky prompt in chat by prefixing the text with `>` using markdown +blockquote syntax. The sticky prompt will be copied at start of every new +prompt in chat window. You can freely edit the sticky prompt, only rule is `>` +prefix at beginning of line. This is useful for preserving stuff like context +and agent selection (see below). Example usage: + +>markdown + > #files + + List all files in the workspace +< + +>markdown + > @models Using Mistral-small + + What is 1 + 11 +< + + +MODELS ~ You can list available models with `:CopilotChatModels` command. Model determines the AI model used for the chat. Default models are: @@ -170,7 +203,7 @@ using `@models` agent from here (example: `@models Using Mistral-small, what is 1 + 11`) -AGENTS +AGENTS ~ Agents are used to determine the AI agent used for the chat. You can list available agents with `:CopilotChatAgents` command. You can set the agent in @@ -183,7 +216,7 @@ You can install more agents from here -CONTEXTS +CONTEXTS ~ Contexts are used to determine the context of the chat. You can set the context in the prompt by using `#` followed by the context name. Supported contexts @@ -306,25 +339,25 @@ Also see here : -- default prompts prompts = { Explain = { - prompt = '/COPILOT_EXPLAIN Write an explanation for the selected code and diagnostics as paragraphs of text.', + prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { - prompt = '/COPILOT_REVIEW Review the selected code.', + prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', callback = function(response, source) -- see config.lua for implementation end, }, Fix = { - prompt = '/COPILOT_GENERATE There is a problem in this code. Rewrite the code to show it with the bug fixed.', + prompt = '> /COPILOT_GENERATE\n\nThere is a problem in this code. Rewrite the code to show it with the bug fixed.', }, Optimize = { - prompt = '/COPILOT_GENERATE Optimize the selected code to improve performance and readability.', + prompt = '> /COPILOT_GENERATE\n\nOptimize the selected code to improve performance and readability.', }, Docs = { - prompt = '/COPILOT_GENERATE Please add documentation comments to the selected code.', + prompt = '> /COPILOT_GENERATE\n\nPlease add documentation comments to the selected code.', }, Tests = { - prompt = '/COPILOT_GENERATE Please generate tests for my code.', + prompt = '> /COPILOT_GENERATE\n\nPlease generate tests for my code.', }, Commit = { prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', @@ -365,6 +398,10 @@ Also see here : normal = '', insert = '' }, + toggle_sticky = { + detail = 'Makes line under cursor sticky or deletes sticky line.', + normal = 'gr', + }, accept_diff = { normal = '', insert = '' From 33c350edae2ff65f85d4a313e45c88b4af1063b4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 17 Nov 2024 15:11:18 +0100 Subject: [PATCH 149/338] Simplify how source buffer is being saved and unify prompt clearing - Instead of passing state.source around, simply update it only if we are outside of chat window and make M.open() noop otherwise - Unify when last prompt is cleared, clear it in .ask always instead of doing it only when explicitely submitting prompt - Unify how selection is checked when simply showing it Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 97 +++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index fec59c1f..20eff419 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -62,7 +62,7 @@ end local function find_lines_between_separator( lines, - start_line, + current_line, start_pattern, end_pattern, allow_end_of_file @@ -72,7 +72,6 @@ local function find_lines_between_separator( end local line_count = #lines - local current_line = vim.api.nvim_win_get_cursor(0)[1] - start_line local separator_line_start = 1 local separator_line_finish = line_count local found_one = false @@ -385,14 +384,20 @@ end --- Open the chat window. ---@param config CopilotChat.config|CopilotChat.config.prompt|nil ----@param source CopilotChat.config.source? -function M.open(config, source) +function M.open(config) + -- If we are already in chat window, do nothing + if state.chat:active() then + return + end + config = vim.tbl_deep_extend('force', M.config, config or {}) state.config = config - state.source = vim.tbl_extend('keep', source or {}, { + + -- Save the source buffer and window (e.g the buffer we are currently asking about) + state.source = { bufnr = vim.api.nvim_get_current_buf(), winnr = vim.api.nvim_get_current_win(), - }) + } utils.return_to_normal_mode() state.chat:open(config) @@ -407,12 +412,11 @@ end --- Toggle the chat window. ---@param config CopilotChat.config|nil ----@param source CopilotChat.config.source? -function M.toggle(config, source) +function M.toggle(config) if state.chat:visible() then M.close() else - M.open(config, source) + M.open(config) end end @@ -472,11 +476,10 @@ end --- Ask a question to the Copilot model. ---@param prompt string ---@param config CopilotChat.config|CopilotChat.config.prompt|nil ----@param source CopilotChat.config.source? -function M.ask(prompt, config, source) +function M.ask(prompt, config) config = vim.tbl_deep_extend('force', M.config, config or {}) vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot_diagnostics')) - M.open(config, source) + M.open(config) prompt = vim.trim(prompt or '') if prompt == '' then @@ -489,6 +492,14 @@ function M.ask(prompt, config, source) finish(config, nil, true) end + -- Clear the current input prompt before asking a new question + local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) + local _, start_line, end_line = + find_lines_between_separator(chat_lines, #chat_lines, M.config.separator .. '$', nil, true) + if #chat_lines == end_line then + vim.api.nvim_buf_set_lines(state.chat.bufnr, start_line, end_line, false, { '' }) + end + state.chat:append(prompt) state.chat:append('\n\n' .. config.answer_header .. config.separator .. '\n\n') @@ -884,17 +895,15 @@ function M.setup(config) map_key(M.config.mappings.submit_prompt, bufnr, function() local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local lines, start_line, end_line = - find_lines_between_separator(chat_lines, 0, M.config.separator .. '$', nil, true) - local input = vim.trim(table.concat(lines, '\n')) - if input ~= '' then - -- If we are entering the input at the end, replace it - if #chat_lines == end_line then - vim.api.nvim_buf_set_lines(bufnr, start_line, end_line, false, { '' }) - end - - M.ask(input, state.config, state.source) - end + local current_line = vim.api.nvim_win_get_cursor(0)[1] + local lines = find_lines_between_separator( + chat_lines, + current_line, + M.config.separator .. '$', + nil, + true + ) + M.ask(vim.trim(table.concat(lines, '\n')), state.config) end) map_key(M.config.mappings.toggle_sticky, bufnr, function() @@ -909,7 +918,7 @@ function M.setup(config) local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local _, start_line, end_line = - find_lines_between_separator(chat_lines, 0, M.config.separator .. '$', nil, true) + find_lines_between_separator(chat_lines, cur_line, M.config.separator .. '$', nil, true) if vim.startswith(current_line, '> ') then return @@ -942,15 +951,20 @@ function M.setup(config) map_key(M.config.mappings.accept_diff, bufnr, function() local selection = get_selection() - if not selection.start_row or not selection.end_row then + if not selection or not selection.start_row or not selection.end_row then return end local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local current_line = vim.api.nvim_win_get_cursor(0)[1] local section_lines, start_line = - find_lines_between_separator(chat_lines, 0, M.config.separator .. '$') - local lines = - find_lines_between_separator(section_lines, start_line - 1, '^```%w+$', '^```$') + find_lines_between_separator(chat_lines, current_line, M.config.separator .. '$') + local lines = find_lines_between_separator( + section_lines, + current_line - start_line - 1, + '^```%w+$', + '^```$' + ) if #lines > 0 then vim.api.nvim_buf_set_text( state.source.bufnr, @@ -965,15 +979,20 @@ function M.setup(config) map_key(M.config.mappings.yank_diff, bufnr, function() local selection = get_selection() - if not selection.start_row or not selection.end_row then + if not selection or not selection.lines then return end local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local current_line = vim.api.nvim_win_get_cursor(0)[1] local section_lines, start_line = - find_lines_between_separator(chat_lines, 0, M.config.separator .. '$') - local lines = - find_lines_between_separator(section_lines, start_line - 1, '^```%w+$', '^```$') + find_lines_between_separator(chat_lines, current_line, M.config.separator .. '$') + local lines = find_lines_between_separator( + section_lines, + current_line - start_line - 1, + '^```%w+$', + '^```$' + ) if #lines > 0 then local content = table.concat(lines, '\n') vim.fn.setreg(M.config.mappings.yank_diff.register, content) @@ -982,15 +1001,21 @@ function M.setup(config) map_key(M.config.mappings.show_diff, bufnr, function() local selection = get_selection() - if not selection or not selection.start_row or not selection.end_row then + if not selection or not selection.lines then return end local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) + local current_line = vim.api.nvim_win_get_cursor(0)[1] local section_lines, start_line = - find_lines_between_separator(chat_lines, 0, M.config.separator .. '$') + find_lines_between_separator(chat_lines, current_line, M.config.separator .. '$') local lines = table.concat( - find_lines_between_separator(section_lines, start_line - 1, '^```%w+$', '^```$'), + find_lines_between_separator( + section_lines, + current_line - start_line - 1, + '^```%w+$', + '^```$' + ), '\n' ) if vim.trim(lines) ~= '' then @@ -1026,7 +1051,7 @@ function M.setup(config) map_key(M.config.mappings.show_user_selection, bufnr, function() local selection = get_selection() - if not selection.start_row or not selection.end_row then + if not selection or not selection.lines then return end From e8a6afa6f30f0be84fced11a2a21172aeaed37ac Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 17 Nov 2024 18:22:11 +0100 Subject: [PATCH 150/338] feat(context): rewrite context system with input and resolving Now contexts are defined in configuration with input and resolve functions. This allows for more flexible and extensible context system. It also makes the system more robust and easier to understand. The changes include: - Add context inputs (with `:` after context) for additional parameters - Add proper model selection (with `$` prefix) - Move context resolution logic to config.lua - Extract git diff to context.gitdiff - Simplify outline/files logic, make it more modular - Improve pattern matching for context/model/agent parsing - Add truncation for large selections Signed-off-by: Tomas Slusny --- README.md | 54 +++++++++++--- lua/CopilotChat/config.lua | 89 +++++++++++++++++++++-- lua/CopilotChat/context.lua | 133 +++++++++++++++++++--------------- lua/CopilotChat/copilot.lua | 4 +- lua/CopilotChat/debuginfo.lua | 4 +- lua/CopilotChat/init.lua | 88 ++++++++++++++++------ lua/CopilotChat/select.lua | 34 --------- 7 files changed, 273 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index 54548d3c..9c106daa 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ What is 1 + 11 ### Models You can list available models with `:CopilotChatModels` command. Model determines the AI model used for the chat. +You can set the model in the prompt by using `$` followed by the model name. Default models are: - `gpt-4o` - This is the default Copilot Chat model. It is a versatile, multimodal model that excels in both text and image processing and is designed to provide fast, reliable responses. It also has superior performance in non-English languages. Gpt-4o is hosted on Azure. @@ -176,11 +177,14 @@ You can install more agents from [here](https://github.com/marketplace?type=apps Contexts are used to determine the context of the chat. You can set the context in the prompt by using `#` followed by the context name. -Supported contexts are: +If context supports input, you can set the input in the prompt by using `:` followed by the input (or pressing `complete` key after `:`). +Default contexts are: +- `buffer` - Includes only the current buffer in chat context. Supports input. - `buffers` - Includes all open buffers in chat context -- `buffer` - Includes only the current buffer in chat context -- `files` - Includes all non-hidden filenames in the current workspace in chat context +- `file` - Includes content of provided file in chat context. Supports input. +- `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. +- `git` - Includes current git diff in chat context. Supports input. ### API @@ -261,17 +265,16 @@ Also see [here](/lua/CopilotChat/config.lua): proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections - system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models + system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). + model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use, 'buffers', 'buffer', 'files' or none (can be specified manually in prompt via #). + context = nil, -- Default context to use (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions answer_header = '## Copilot ', -- Header to use for AI answers error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input @@ -280,6 +283,7 @@ Also see [here](/lua/CopilotChat/config.lua): insert_at_end = false, -- Move cursor to end of buffer when inserting text clear_chat_on_new_prompt = false, -- Clears chat on every new prompt highlight_selection = true, -- Highlight selection in the source buffer when in the chat window + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received @@ -289,16 +293,43 @@ Also see [here](/lua/CopilotChat/config.lua): return select.visual(source) or select.buffer(source) end, + -- default contexts + contexts = { + buffer = { + -- see config.lua for implementation + input = function(callback) end, + resolve = function(input, source) end, + }, + buffers = { + -- see config.lua for implementation + resolve = function(input, source) end, + }, + file = { + -- see config.lua for implementation + input = function(callback) end, + resolve = function(input, source) end, + }, + files = { + -- see config.lua for implementation + input = function(callback) end, + resolve = function(input, source) end, + }, + git = { + -- see config.lua for implementation + input = function(callback) end, + resolve = function(input, source) end, + }, + }, + -- default prompts prompts = { Explain = { prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { + -- see config.lua for implementation prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', - callback = function(response, source) - -- see config.lua for implementation - end, + callback = function(response, source) end, }, Fix = { prompt = '> /COPILOT_GENERATE\n\nThere is a problem in this code. Rewrite the code to show it with the bug fixed.', @@ -313,8 +344,7 @@ Also see [here](/lua/CopilotChat/config.lua): prompt = '> /COPILOT_GENERATE\n\nPlease generate tests for my code.', }, Commit = { - prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', - selection = select.gitdiff, + prompt = '> #git:staged\n\nWrite commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', }, }, diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index a9d65866..2ac95e57 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -1,4 +1,5 @@ local prompts = require('CopilotChat.prompts') +local context = require('CopilotChat.context') local select = require('CopilotChat.select') --- @class CopilotChat.config.source @@ -23,6 +24,11 @@ local select = require('CopilotChat.select') ---@field end_row number? ---@field end_col number? +---@class CopilotChat.config.context +---@field description string? +---@field input fun(callback: fun(input: string?))? +---@field resolve fun(input: string?, source: CopilotChat.config.source):table + ---@class CopilotChat.config.prompt ---@field prompt string? ---@field description string? @@ -83,9 +89,11 @@ local select = require('CopilotChat.select') ---@field auto_insert_mode boolean? ---@field clear_chat_on_new_prompt boolean? ---@field highlight_selection boolean? +---@field highlight_headers boolean? ---@field history_path string? ---@field callback fun(response: string, source: CopilotChat.config.source)? ---@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.config.selection? +---@field contexts table? ---@field prompts table? ---@field window CopilotChat.config.window? ---@field mappings CopilotChat.config.mappings? @@ -95,17 +103,16 @@ return { proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections - system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models + system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). + model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use, 'buffers', 'buffer', 'files' or none (can be specified manually in prompt via #). + context = nil, -- Default context to use (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions answer_header = '## Copilot ', -- Header to use for AI answers error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input @@ -114,6 +121,7 @@ return { insert_at_end = false, -- Move cursor to end of buffer when inserting text clear_chat_on_new_prompt = false, -- Clears chat on every new prompt highlight_selection = true, -- Highlight selection + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received @@ -123,6 +131,76 @@ return { return select.visual(source) or select.buffer(source) end, + -- default contexts + contexts = { + buffer = { + description = 'Includes only the current buffer in chat context. Supports input.', + input = function(callback) + vim.ui.select(vim.api.nvim_list_bufs(), { + prompt = 'Select a buffer> ', + }, callback) + end, + resolve = function(input, source) + return { + context.outline(input and tonumber(input) or source.bufnr), + } + end, + }, + buffers = { + description = 'Includes all open buffers in chat context.', + resolve = function() + return vim.tbl_map( + context.outline, + vim.tbl_filter(function(b) + return vim.api.nvim_buf_is_loaded(b) and vim.fn.buflisted(b) == 1 + end, vim.api.nvim_list_bufs()) + ) + end, + }, + file = { + description = 'Includes content of provided file in chat context. Supports input.', + input = function(callback) + local files = vim.tbl_filter(function(file) + return vim.fn.isdirectory(file) == 0 + end, vim.fn.glob('**/*', false, true)) + + vim.ui.select(files, { + prompt = 'Select a file> ', + }, callback) + end, + resolve = function(input) + return { + context.file(input), + } + end, + }, + files = { + description = 'Includes all non-hidden filenames in the current workspace in chat context. Supports input.', + input = function(callback) + vim.ui.input({ + prompt = 'Enter a file pattern> ', + default = '**/*', + }, callback) + end, + resolve = function(input) + return context.files(input) + end, + }, + git = { + description = 'Includes current git diff in chat context. Supports input.', + input = function(callback) + vim.ui.select({ 'unstaged', 'staged' }, { + prompt = 'Select diff type> ', + }, callback) + end, + resolve = function(input, source) + return { + context.gitdiff(input, source.bufnr), + } + end, + }, + }, + -- default prompts prompts = { Explain = { @@ -183,8 +261,7 @@ return { prompt = '> /COPILOT_GENERATE\n\nPlease generate tests for my code.', }, Commit = { - prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', - selection = select.gitdiff, + prompt = '> #git:staged\n\nWrite commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', }, }, diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index f50442f4..3330939e 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -1,4 +1,3 @@ -local async = require('plenary.async') local log = require('plenary.log') local M = {} @@ -42,6 +41,7 @@ local off_side_rule_languages = { } local big_file_threshold = 500 +local selection_threshold = 200 local function spatial_distance_cosine(a, b) local dot_product = 0 @@ -73,46 +73,13 @@ local function data_ranked_by_relatedness(query, data, top_n) return result end -local get_context_data = async.wrap(function(context, bufnr, callback) - vim.schedule(function() - local outline = {} - if context == 'buffers' then - outline = vim.tbl_map( - M.build_outline, - vim.tbl_filter(function(b) - return vim.api.nvim_buf_is_loaded(b) and vim.fn.buflisted(b) == 1 - end, vim.api.nvim_list_bufs()) - ) - elseif context == 'buffer' then - table.insert(outline, M.build_outline(bufnr)) - elseif context == 'files' then - outline = M.build_file_map() - end - - callback(outline) - end) -end, 3) - ---- Get supported contexts ----@return table -function M.supported_contexts() - return { - buffers = 'Includes all open buffers in chat context', - buffer = 'Includes only the current buffer in chat context', - files = 'Includes all non-hidden filenames in the current workspace in chat context', - } -end - --- Get list of all files in workspace +---@param pattern string? ---@return table -function M.build_file_map() - -- Use vim.fn.glob() to get all files - local files = vim.fn.glob('**/*', false, true) - - -- Filter out directories - files = vim.tbl_filter(function(file) +function M.files(pattern) + local files = vim.tbl_filter(function(file) return vim.fn.isdirectory(file) == 0 - end, files) + end, vim.fn.glob(pattern or '**/*', false, true)) if #files == 0 then return {} @@ -138,18 +105,33 @@ function M.build_file_map() return out end +--- Get the content of a file +---@param filename string +---@return CopilotChat.copilot.embed? +function M.file(filename) + local content = vim.fn.readfile(filename) + if #content == 0 then + return + end + + return { + content = table.concat(content, '\n'), + filename = filename, + filetype = vim.filetype.match({ filename = filename }), + } +end + --- Build an outline for a buffer --- FIXME: Handle multiline function argument definitions when building the outline ---@param bufnr number ----@param force_outline boolean? If true, always build the outline ---@return CopilotChat.copilot.embed? -function M.build_outline(bufnr, force_outline) +function M.outline(bufnr) local name = vim.api.nvim_buf_get_name(bufnr) local ft = vim.bo[bufnr].filetype -- If buffer is not too big, just return the content local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - if not force_outline and #lines < big_file_threshold then + if #lines < big_file_threshold then return { content = table.concat(lines, '\n'), filename = name, @@ -245,38 +227,72 @@ function M.build_outline(bufnr, force_outline) } end +--- Get current git diff +---@param type string? +---@param bufnr number +function M.gitdiff(type, bufnr) + type = type or 'unstaged' + local bufname = vim.api.nvim_buf_get_name(bufnr) + local file_path = bufname:gsub('^%w+://', '') + local dir = vim.fn.fnamemodify(file_path, ':h') + if not dir or dir == '' then + return nil + end + dir = dir:gsub('.git$', '') + + local cmd = 'git -C ' .. dir .. ' diff --no-color --no-ext-diff' + + if type == 'staged' then + cmd = cmd .. ' --staged' + end + + local handle = io.popen(cmd) + if not handle then + return nil + end + + local result = handle:read('*a') + handle:close() + if not result or result == '' then + return nil + end + + return { + content = result, + filename = 'git_diff_' .. type, + filetype = 'diff', + } +end + ---@class CopilotChat.context.find_for_query.opts ----@field context string? +---@field embeddings table ---@field prompt string ---@field selection string? ---@field filename string ---@field filetype string ----@field bufnr number ---- Find items for a query +--- Filter embeddings based on the query ---@param copilot CopilotChat.Copilot ---@param opts CopilotChat.context.find_for_query.opts ---@return table -function M.find_for_query(copilot, opts) - local context = opts.context +function M.filter_embeddings(copilot, opts) + local embeddings = opts.embeddings local prompt = opts.prompt local selection = opts.selection local filename = opts.filename local filetype = opts.filetype - local bufnr = opts.bufnr - - local outline = get_context_data(context, bufnr) - outline = vim.tbl_filter(function(item) - return item ~= nil - end, outline) - if #outline == 0 then + local out = copilot:embed(embeddings) + if #out == 0 then return {} end - local out = copilot:embed(outline) - if #out == 0 then - return {} + -- If selection is too big, truncate it + if selection then + local lines = vim.split(selection, '\n') + selection = #lines > selection_threshold + and table.concat(vim.list_slice(lines, 1, selection_threshold), '\n') + or selection end log.debug(string.format('Got %s embeddings', #out)) @@ -294,13 +310,16 @@ function M.find_for_query(copilot, opts) if not query then return {} end + + local data = data_ranked_by_relatedness(query, out, 20) + log.debug('Prompt:', query.prompt) log.debug('Content:', query.content) - local data = data_ranked_by_relatedness(query, out, 20) log.debug('Ranked data:', #data) for i, item in ipairs(data) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end + return data end diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index ab41ec88..7d4254b9 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -638,7 +638,9 @@ function Copilot:ask(prompt, opts) embeddings_prompt = embeddings_prompt .. string.format('[#file:%s](#file:%s-context)\n', embedding.filename, embedding.filename) end - prompt = embeddings_prompt .. prompt + if embeddings_prompt ~= '' then + prompt = embeddings_prompt .. '\n' .. prompt + end local last_message = nil local errored = false diff --git a/lua/CopilotChat/debuginfo.lua b/lua/CopilotChat/debuginfo.lua index 365c36ee..ec10daad 100644 --- a/lua/CopilotChat/debuginfo.lua +++ b/lua/CopilotChat/debuginfo.lua @@ -18,7 +18,7 @@ function M.open() '', } - local outline = context.build_outline(vim.api.nvim_get_current_buf(), true) + local outline = context.outline(vim.api.nvim_get_current_buf()) if outline then table.insert(lines, 'Current buffer outline:') table.insert(lines, '`' .. outline.filename .. '`') @@ -30,7 +30,7 @@ function M.open() table.insert(lines, '```') end - local files = context.build_file_map() + local files = context.files() if files then table.insert(lines, 'Current workspace file map:') table.insert(lines, '```text') diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 20eff419..e71aed78 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -264,8 +264,8 @@ end ---@return table function M.complete_info() return { - triggers = { '@', '/', '#' }, - pattern = [[\%(@\|/\|#\)\k*]], + triggers = { '@', '/', '#', '$' }, + pattern = [[\%(@\|/\|#\|\$\)\S*]], } end @@ -273,8 +273,8 @@ end ---@param callback function(table) function M.complete_items(callback) async.run(function() + local models = state.copilot:list_models() local agents = state.copilot:list_agents() - local contexts = context.supported_contexts() local items = {} local prompts_to_use = M.prompts() @@ -290,9 +290,20 @@ function M.complete_items(callback) } end - for agent, description in pairs(agents) do + for name, description in pairs(models) do items[#items + 1] = { - word = '@' .. agent, + word = '$' .. name, + kind = 'model', + menu = description, + icase = 1, + dup = 0, + empty = 0, + } + end + + for name, description in pairs(agents) do + items[#items + 1] = { + word = '@' .. name, kind = 'agent', menu = description, icase = 1, @@ -301,11 +312,11 @@ function M.complete_items(callback) } end - for prompt_context, description in pairs(contexts) do + for name, value in pairs(M.config.contexts) do items[#items + 1] = { - word = '#' .. prompt_context, + word = '#' .. name, kind = 'context', - menu = description, + menu = value.description or '', icase = 1, dup = 0, empty = 0, @@ -519,27 +530,45 @@ function M.ask(prompt, config) )) or 'untitled' - local contexts = vim.tbl_keys(context.supported_contexts()) - local selected_context = config.context - for prompt_context in prompt:gmatch('#([%w_-]+)') do - if vim.tbl_contains(contexts, prompt_context) then - selected_context = prompt_context - prompt = string.gsub(prompt, '#' .. prompt_context .. '%s*', '') + local embeddings = {} + for prompt_context in prompt:gmatch('#([^%s]+)') do + local split = vim.split(prompt_context, ':') + local context_name = split[1] + local context_input = split[2] + local context_value = config.contexts[context_name] + + if context_value then + for _, embedding in ipairs(context_value.resolve(context_input, state.source)) do + if embedding then + table.insert(embeddings, embedding) + end + end + + prompt = prompt:gsub('#' .. prompt_context .. '%s*', '') end end async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) local selected_agent = config.agent - for agent in prompt:gmatch('@([%w_-]+)') do + for agent in prompt:gmatch('@([^%s]+)') do if vim.tbl_contains(agents, agent) then selected_agent = agent prompt = prompt:gsub('@' .. agent .. '%s*', '') end end - local query_ok, embeddings = pcall(context.find_for_query, state.copilot, { - context = selected_context, + local models = vim.tbl_keys(state.copilot:list_models()) + local selected_model = config.model + for model in prompt:gmatch('%$([^%s]+)') do + if vim.tbl_contains(models, model) then + selected_model = model + prompt = prompt:gsub('%$' .. model .. '%s*', '') + end + end + + local query_ok, filtered_embeddings = pcall(context.filter_embeddings, state.copilot, { + embeddings = embeddings, prompt = prompt, selection = selection.lines, filename = filename, @@ -549,7 +578,7 @@ function M.ask(prompt, config) if not query_ok then vim.schedule(function() - show_error(embeddings, config) + show_error(filtered_embeddings, config) end) return end @@ -557,11 +586,11 @@ function M.ask(prompt, config) local ask_ok, response, token_count, token_max_count = pcall(state.copilot.ask, state.copilot, prompt, { selection = selection, - embeddings = embeddings, + embeddings = filtered_embeddings, filename = filename, filetype = filetype, system_prompt = system_prompt, - model = config.model, + model = selected_model, agent = selected_agent, temperature = config.temperature, on_progress = function(token) @@ -873,7 +902,9 @@ function M.setup(config) map_key(M.config.mappings.complete, bufnr, function() local info = M.complete_info() local line = vim.api.nvim_get_current_line() - local col = vim.api.nvim_win_get_cursor(0)[2] + local cursor = vim.api.nvim_win_get_cursor(0) + local row = cursor[1] + local col = cursor[2] if col == 0 or #line == 0 then return end @@ -883,6 +914,21 @@ function M.setup(config) return end + if vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then + local found_context = M.config.contexts[prefix:sub(2, -2)] + if found_context and found_context.input then + found_context.input(function(value) + if not value then + return + end + + vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { tostring(value) }) + end) + end + + return + end + M.complete_items(function(items) vim.fn.complete( cmp_start + 1, diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 5dbb9f59..e6f2c6f0 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -171,38 +171,4 @@ function M.clipboard() } end ---- Select and process current git diff ---- @param source CopilotChat.config.source ---- @return CopilotChat.config.selection|nil -function M.gitdiff(source) - local select_buffer = M.buffer(source) - if not select_buffer then - return nil - end - - local bufname = vim.api.nvim_buf_get_name(source.bufnr) - local file_path = bufname:gsub('^%w+://', '') - local dir = vim.fn.fnamemodify(file_path, ':h') - if not dir or dir == '' then - return nil - end - dir = dir:gsub('.git$', '') - - local cmd = 'git -C ' .. dir .. ' diff --no-color --no-ext-diff --staged' - local handle = io.popen(cmd) - if not handle then - return nil - end - - local result = handle:read('*a') - handle:close() - if not result or result == '' then - return nil - end - - select_buffer.filetype = 'diff' - select_buffer.lines = result - return select_buffer -end - return M From 98c9f1dd125590b0256c6a3d904e2a9499845292 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 Nov 2024 19:53:36 +0000 Subject: [PATCH 151/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 58 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 29708940..fdda90b7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -189,7 +189,8 @@ and agent selection (see below). Example usage: MODELS ~ You can list available models with `:CopilotChatModels` command. Model -determines the AI model used for the chat. Default models are: +determines the AI model used for the chat. You can set the model in the prompt +by using `$` followed by the model name. Default models are: - `gpt-4o` - This is the default Copilot Chat model. It is a versatile, multimodal model that excels in both text and image processing and is designed to provide fast, reliable responses. It also has superior performance in non-English languages. Gpt-4o is hosted on Azure. - `claude-3.5-sonnet` - This model excels at coding tasks across the entire software development lifecycle, from initial design to bug fixes, maintenance to optimizations. GitHub Copilot uses Claude 3.5 Sonnet hosted on Amazon Web Services. @@ -219,12 +220,15 @@ You can install more agents from here CONTEXTS ~ Contexts are used to determine the context of the chat. You can set the context -in the prompt by using `#` followed by the context name. Supported contexts -are: +in the prompt by using `#` followed by the context name. If context supports +input, you can set the input in the prompt by using `:` followed by the input +(or pressing `complete` key after `:`). Default contexts are: +- `buffer` - Includes only the current buffer in chat context. Supports input. - `buffers` - Includes all open buffers in chat context -- `buffer` - Includes only the current buffer in chat context -- `files` - Includes all non-hidden filenames in the current workspace in chat context +- `file` - Includes content of provided file in chat context. Supports input. +- `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. +- `git` - Includes current git diff in chat context. Supports input. API ~ @@ -308,17 +312,16 @@ Also see here : proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections - system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use - model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models + system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). + model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use, 'buffers', 'buffer', 'files' or none (can be specified manually in prompt via #). + context = nil, -- Default context to use (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions answer_header = '## Copilot ', -- Header to use for AI answers error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input @@ -327,6 +330,7 @@ Also see here : insert_at_end = false, -- Move cursor to end of buffer when inserting text clear_chat_on_new_prompt = false, -- Clears chat on every new prompt highlight_selection = true, -- Highlight selection in the source buffer when in the chat window + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received @@ -336,16 +340,43 @@ Also see here : return select.visual(source) or select.buffer(source) end, + -- default contexts + contexts = { + buffer = { + -- see config.lua for implementation + input = function(callback) end, + resolve = function(input, source) end, + }, + buffers = { + -- see config.lua for implementation + resolve = function(input, source) end, + }, + file = { + -- see config.lua for implementation + input = function(callback) end, + resolve = function(input, source) end, + }, + files = { + -- see config.lua for implementation + input = function(callback) end, + resolve = function(input, source) end, + }, + git = { + -- see config.lua for implementation + input = function(callback) end, + resolve = function(input, source) end, + }, + }, + -- default prompts prompts = { Explain = { prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { + -- see config.lua for implementation prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', - callback = function(response, source) - -- see config.lua for implementation - end, + callback = function(response, source) end, }, Fix = { prompt = '> /COPILOT_GENERATE\n\nThere is a problem in this code. Rewrite the code to show it with the bug fixed.', @@ -360,8 +391,7 @@ Also see here : prompt = '> /COPILOT_GENERATE\n\nPlease generate tests for my code.', }, Commit = { - prompt = 'Write commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', - selection = select.gitdiff, + prompt = '> #git:staged\n\nWrite commit message for the change with commitizen convention. Make sure the title has maximum 50 characters and message is wrapped at 72 characters. Wrap the whole message in code block with language gitcommit.', }, }, From 4d2a2c61cdbe1a375cb14b45b659eef8e8633f9c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 17 Nov 2024 21:59:26 +0100 Subject: [PATCH 152/338] docs: add missing help for model selection Add missing help line for model selection using `$` syntax in the chat help overview. --- lua/CopilotChat/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index e71aed78..162b81f0 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -872,6 +872,7 @@ function M.setup(config) chat_help = chat_help .. '`@` to select an agent\n' chat_help = chat_help .. '`#` to select a context\n' chat_help = chat_help .. '`/` to select a prompt\n' + chat_help = chat_help .. '`$` to select a model\n' chat_help = chat_help .. '`> ` to make a sticky prompt (copied to next prompt)\n' chat_help = chat_help .. '\n**`Mappings`**\n' From b58f46ac012bbb19d589b168d030f4c760083d22 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 17 Nov 2024 22:24:10 +0100 Subject: [PATCH 153/338] docs: improve documentation for contexts and complete binding Signed-off-by: Tomas Slusny --- README.md | 18 ++++++------------ lua/CopilotChat/config.lua | 1 - 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9c106daa..6602fa9e 100644 --- a/README.md +++ b/README.md @@ -296,28 +296,24 @@ Also see [here](/lua/CopilotChat/config.lua): -- default contexts contexts = { buffer = { + description = 'Includes only the current buffer in chat context. Supports input.', -- see config.lua for implementation - input = function(callback) end, - resolve = function(input, source) end, }, buffers = { + description = 'Includes all open buffers in chat context.', -- see config.lua for implementation - resolve = function(input, source) end, }, file = { + description = 'Includes content of provided file in chat context. Supports input.', -- see config.lua for implementation - input = function(callback) end, - resolve = function(input, source) end, }, files = { + description = 'Includes all non-hidden filenames in the current workspace in chat context. Supports input.', -- see config.lua for implementation - input = function(callback) end, - resolve = function(input, source) end, }, git = { + description = 'Includes current git diff in chat context. Supports input.', -- see config.lua for implementation - input = function(callback) end, - resolve = function(input, source) end, }, }, @@ -327,9 +323,8 @@ Also see [here](/lua/CopilotChat/config.lua): prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { - -- see config.lua for implementation prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', - callback = function(response, source) end, + -- see config.lua for implementation }, Fix = { prompt = '> /COPILOT_GENERATE\n\nThere is a problem in this code. Rewrite the code to show it with the bug fixed.', @@ -366,7 +361,6 @@ Also see [here](/lua/CopilotChat/config.lua): -- default mappings mappings = { complete = { - detail = 'Use @ or / for options.', insert ='', }, close = { diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 2ac95e57..4904ddc7 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -283,7 +283,6 @@ return { -- default mappings mappings = { complete = { - detail = 'Use @ or / for options.', insert = '', }, close = { From ec7c2469b28a1377914fffcdd7118cf630316102 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 Nov 2024 21:25:10 +0000 Subject: [PATCH 154/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index fdda90b7..39359c2f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -343,28 +343,24 @@ Also see here : -- default contexts contexts = { buffer = { + description = 'Includes only the current buffer in chat context. Supports input.', -- see config.lua for implementation - input = function(callback) end, - resolve = function(input, source) end, }, buffers = { + description = 'Includes all open buffers in chat context.', -- see config.lua for implementation - resolve = function(input, source) end, }, file = { + description = 'Includes content of provided file in chat context. Supports input.', -- see config.lua for implementation - input = function(callback) end, - resolve = function(input, source) end, }, files = { + description = 'Includes all non-hidden filenames in the current workspace in chat context. Supports input.', -- see config.lua for implementation - input = function(callback) end, - resolve = function(input, source) end, }, git = { + description = 'Includes current git diff in chat context. Supports input.', -- see config.lua for implementation - input = function(callback) end, - resolve = function(input, source) end, }, }, @@ -374,9 +370,8 @@ Also see here : prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', }, Review = { - -- see config.lua for implementation prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', - callback = function(response, source) end, + -- see config.lua for implementation }, Fix = { prompt = '> /COPILOT_GENERATE\n\nThere is a problem in this code. Rewrite the code to show it with the bug fixed.', @@ -413,7 +408,6 @@ Also see here : -- default mappings mappings = { complete = { - detail = 'Use @ or / for options.', insert ='', }, close = { From 9ea6718c5e715d3264ea9789c309dd448ee4837f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 17 Nov 2024 22:41:02 +0100 Subject: [PATCH 155/338] Fix loading of default context Accidentally forgot to load it after reworking how contexts work Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 162b81f0..45e4d28e 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -531,7 +531,7 @@ function M.ask(prompt, config) or 'untitled' local embeddings = {} - for prompt_context in prompt:gmatch('#([^%s]+)') do + local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') local context_name = split[1] local context_input = split[2] @@ -548,6 +548,14 @@ function M.ask(prompt, config) end end + if config.context then + parse_context(config.context) + end + + for prompt_context in prompt:gmatch('#([^%s]+)') do + parse_context(prompt_context) + end + async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) local selected_agent = config.agent From 93a8964f84734217e5c25ffc3d7c41ab1bd2c50e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 02:37:40 +0100 Subject: [PATCH 156/338] Load chat auto_insert from config instead of passing it in contructor This means it can be properly overriden when opening chat and also follows same loading as other chat configuration already does Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 5 +++-- lua/CopilotChat/init.lua | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 139d6836..ba80787a 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -30,15 +30,15 @@ function CopilotChatFoldExpr(lnum, separator) return '=' end -local Chat = class(function(self, help, auto_insert, on_buf_create) +local Chat = class(function(self, help, on_buf_create) self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') self.help = help - self.auto_insert = auto_insert self.on_buf_create = on_buf_create self.bufnr = nil self.winnr = nil self.spinner = nil self.separator = nil + self.auto_insert = false self.auto_follow_cursor = true self.highlight_headers = true self.layout = nil @@ -220,6 +220,7 @@ function Chat:open(config) self.layout = layout self.separator = config.separator + self.auto_insert = config.auto_insert self.auto_follow_cursor = config.auto_follow_cursor self.highlight_headers = config.highlight_headers diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 45e4d28e..5d0f666c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -873,7 +873,6 @@ function M.setup(config) end state.chat = Chat( M.config.show_help and key_to_info('show_help', M.config.mappings.show_help), - M.config.auto_insert_mode, function(bufnr) map_key(M.config.mappings.show_help, bufnr, function() local chat_help = '**`Special tokens`**\n' From 72595b8f0c93bfca6d3b640acb666efcbc2e9c13 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Nov 2024 01:41:29 +0000 Subject: [PATCH 157/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 39359c2f..8d29015c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 17 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 18 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 9d07da5d5bfd7d1cb1a4a2d2f65e84bcf5788718 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 17 Nov 2024 21:10:41 +0100 Subject: [PATCH 158/338] 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 --- README.md | 25 +----- lua/CopilotChat/config.lua | 2 + lua/CopilotChat/init.lua | 130 +++++++++++++++------------ lua/CopilotChat/integrations/cmp.lua | 47 +--------- lua/CopilotChat/utils.lua | 29 +++++- 5 files changed, 104 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 6602fa9e..1ca4b265 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,7 @@ Also see [here](/lua/CopilotChat/config.lua): error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat + chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input 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. -
-nvim-cmp integration - -Requires [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) plugin to be installed (and properly configured). - -```lua --- Registers copilot-chat source and enables it for copilot-chat filetype (so copilot chat window) -require("CopilotChat.integrations.cmp").setup() - --- You might also want to disable default complete mapping for copilot chat when doing this -require('CopilotChat').setup({ - mappings = { - complete = { - insert = '', - }, - }, - -- rest of your config -}) -``` - -![image](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/063fc99f-a4b2-4187-a065-0fdd287ebee2) - -
-
render-markdown integration diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 4904ddc7..7f364cf8 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -83,6 +83,7 @@ local select = require('CopilotChat.select') ---@field answer_header string? ---@field error_header string? ---@field separator string? +---@field chat_autocomplete boolean? ---@field show_folds boolean? ---@field show_help boolean? ---@field auto_follow_cursor boolean? @@ -114,6 +115,7 @@ return { error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat + chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input auto_follow_cursor = true, -- Auto-follow cursor in chat diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 5d0f666c..9341da11 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -44,22 +44,6 @@ local state = { help = nil, } -local function blend_color_with_neovim_bg(color_name, blend) - local color_int = vim.api.nvim_get_hl(0, { name = color_name }).fg - local bg_int = vim.api.nvim_get_hl(0, { name = 'Normal' }).bg - - if not color_int or not bg_int then - return - end - - local color = { (color_int / 65536) % 256, (color_int / 256) % 256, color_int % 256 } - local bg = { (bg_int / 65536) % 256, (bg_int / 256) % 256, bg_int % 256 } - local r = math.floor((color[1] * blend + bg[1] * (100 - blend)) / 100) - local g = math.floor((color[2] * blend + bg[2] * (100 - blend)) / 100) - local b = math.floor((color[3] * blend + bg[3] * (100 - blend)) / 100) - return string.format('#%02x%02x%02x', r, g, b) -end - local function find_lines_between_separator( lines, current_line, @@ -260,6 +244,47 @@ local function key_to_info(name, key, surround) return out end +local function trigger_complete() + local info = M.complete_info() + local bufnr = vim.api.nvim_get_current_buf() + local line = vim.api.nvim_get_current_line() + local cursor = vim.api.nvim_win_get_cursor(0) + local row = cursor[1] + local col = cursor[2] + if col == 0 or #line == 0 then + return + end + + local prefix, cmp_start = unpack(vim.fn.matchstrpos(line:sub(1, col), info.pattern)) + if not prefix then + return + end + + if vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then + local found_context = M.config.contexts[prefix:sub(2, -2)] + if found_context and found_context.input then + found_context.input(function(value) + if not value then + return + end + + vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { tostring(value) }) + end) + end + + return + end + + M.complete_items(function(items) + vim.fn.complete( + cmp_start + 1, + vim.tbl_filter(function(item) + return vim.startswith(item.word:lower(), prefix:lower()) + end, items) + ) + end) +end + --- Get the completion info for the chat window, for use with custom completion providers ---@return table function M.complete_info() @@ -275,8 +300,8 @@ function M.complete_items(callback) async.run(function() local models = state.copilot:list_models() local agents = state.copilot:list_agents() - local items = {} local prompts_to_use = M.prompts() + local items = {} for name, prompt in pairs(prompts_to_use) do items[#items + 1] = { @@ -323,6 +348,10 @@ function M.complete_items(callback) } end + table.sort(items, function(a, b) + return a.kind < b.kind + end) + vim.schedule(function() callback(items) end) @@ -784,9 +813,17 @@ function M.setup(config) end local hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights') - vim.api.nvim_set_hl(hl_ns, '@diff.plus', { bg = blend_color_with_neovim_bg('DiffAdd', 20) }) - vim.api.nvim_set_hl(hl_ns, '@diff.minus', { bg = blend_color_with_neovim_bg('DiffDelete', 20) }) - vim.api.nvim_set_hl(hl_ns, '@diff.delta', { bg = blend_color_with_neovim_bg('DiffChange', 20) }) + vim.api.nvim_set_hl(hl_ns, '@diff.plus', { bg = utils.blend_color_with_neovim_bg('DiffAdd', 20) }) + vim.api.nvim_set_hl( + hl_ns, + '@diff.minus', + { bg = utils.blend_color_with_neovim_bg('DiffDelete', 20) } + ) + vim.api.nvim_set_hl( + hl_ns, + '@diff.delta', + { bg = utils.blend_color_with_neovim_bg('DiffChange', 20) } + ) vim.api.nvim_set_hl(0, 'CopilotChatSpinner', { link = 'CursorColumn', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) @@ -906,46 +943,23 @@ function M.setup(config) map_key(M.config.mappings.reset, bufnr, M.reset) map_key(M.config.mappings.close, bufnr, M.close) + map_key(M.config.mappings.complete, bufnr, trigger_complete) - map_key(M.config.mappings.complete, bufnr, function() - local info = M.complete_info() - local line = vim.api.nvim_get_current_line() - local cursor = vim.api.nvim_win_get_cursor(0) - local row = cursor[1] - local col = cursor[2] - if col == 0 or #line == 0 then - return - end - - local prefix, cmp_start = unpack(vim.fn.matchstrpos(line:sub(1, col), info.pattern)) - if not prefix then - return - end - - if vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then - local found_context = M.config.contexts[prefix:sub(2, -2)] - if found_context and found_context.input then - found_context.input(function(value) - if not value then - return - end - - vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { tostring(value) }) - end) - end - - return - end + if M.config.chat_autocomplete then + vim.api.nvim_create_autocmd('TextChangedI', { + buffer = bufnr, + callback = function() + local line = vim.api.nvim_get_current_line() + local cursor = vim.api.nvim_win_get_cursor(0) + local col = cursor[2] + local char = line:sub(col, col) - M.complete_items(function(items) - vim.fn.complete( - cmp_start + 1, - vim.tbl_filter(function(item) - return vim.startswith(item.word:lower(), prefix:lower()) - end, items) - ) - end) - end) + if vim.tbl_contains(M.complete_info().triggers, char) then + utils.debounce(trigger_complete, 100) + end + end, + }) + end map_key(M.config.mappings.submit_prompt, bufnr, function() local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) diff --git a/lua/CopilotChat/integrations/cmp.lua b/lua/CopilotChat/integrations/cmp.lua index 47928762..0568cac8 100644 --- a/lua/CopilotChat/integrations/cmp.lua +++ b/lua/CopilotChat/integrations/cmp.lua @@ -1,52 +1,9 @@ -local cmp = require('cmp') -local chat = require('CopilotChat') - -local Source = {} - -function Source:get_trigger_characters() - return chat.complete_info().triggers -end - -function Source:get_keyword_pattern() - return chat.complete_info().pattern -end - -function Source:complete(params, callback) - chat.complete_items(function(items) - items = vim.tbl_map(function(item) - return { - label = item.word, - kind = cmp.lsp.CompletionItemKind.Keyword, - } - end, items) - - local prefix = string.lower(params.context.cursor_before_line:sub(params.offset)) - - callback({ - items = vim.tbl_filter(function(item) - return vim.startswith(item.label:lower(), prefix:lower()) - end, items), - }) - end) -end - ----@param completion_item lsp.CompletionItem ----@param callback fun(completion_item: lsp.CompletionItem|nil) -function Source:execute(completion_item, callback) - callback(completion_item) - vim.api.nvim_set_option_value('buflisted', false, { buf = 0 }) -end +local utils = require('CopilotChat.utils') local M = {} ---- Setup the nvim-cmp source for copilot-chat window function M.setup() - cmp.register_source('copilot-chat', Source) - cmp.setup.filetype('copilot-chat', { - sources = { - { name = 'copilot-chat' }, - }, - }) + utils.deprecate('CopilotChat.integrations.cmp.setup', 'config.chat_autocomplete=true') end return M diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index d979d85a..85a1f776 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,4 +1,3 @@ -local log = require('plenary.log') local M = {} --- Create class @@ -74,6 +73,23 @@ function M.table_equals(a, b) return true end +--- Blend a color with the neovim background +function M.blend_color_with_neovim_bg(color_name, blend) + local color_int = vim.api.nvim_get_hl(0, { name = color_name }).fg + local bg_int = vim.api.nvim_get_hl(0, { name = 'Normal' }).bg + + if not color_int or not bg_int then + return + end + + local color = { (color_int / 65536) % 256, (color_int / 256) % 256, color_int % 256 } + local bg = { (bg_int / 65536) % 256, (bg_int / 256) % 256, bg_int % 256 } + local r = math.floor((color[1] * blend + bg[1] * (100 - blend)) / 100) + local g = math.floor((color[2] * blend + bg[2] * (100 - blend)) / 100) + local b = math.floor((color[3] * blend + bg[3] * (100 - blend)) / 100) + return string.format('#%02x%02x%02x', r, g, b) +end + --- Return to normal mode function M.return_to_normal_mode() local mode = vim.fn.mode():lower() @@ -84,8 +100,17 @@ function M.return_to_normal_mode() end end +--- Mark a function as deprecated function M.deprecate(old, new) - vim.deprecate(old, new, '3.0.0', 'CopilotChat.nvim', false) + vim.deprecate(old, new, '3.0.X', 'CopilotChat.nvim', false) +end + +--- Debounce a function +function M.debounce(fn, delay) + if M.timer then + M.timer:stop() + end + M.timer = vim.defer_fn(fn, delay) end return M From edea00537f1550bcdf4ee91b2d8bb2131b143675 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Nov 2024 08:41:50 +0000 Subject: [PATCH 159/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 8d29015c..0b8070a7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -323,6 +323,7 @@ Also see here : error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat + chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) show_folds = true, -- Shows folds for sections in chat show_help = true, -- Shows help message as virtual lines when waiting for user input auto_follow_cursor = true, -- Auto-follow cursor in chat @@ -621,26 +622,6 @@ Requires fzf-lua plugin to be installed. }, < -nvim-cmp integration ~ - -Requires nvim-cmp plugin to be installed -(and properly configured). - ->lua - -- Registers copilot-chat source and enables it for copilot-chat filetype (so copilot chat window) - require("CopilotChat.integrations.cmp").setup() - - -- You might also want to disable default complete mapping for copilot chat when doing this - require('CopilotChat').setup({ - mappings = { - complete = { - insert = '', - }, - }, - -- rest of your config - }) -< - render-markdown integration ~ Requires render-markdown @@ -714,9 +695,8 @@ STARGAZERS OVER TIME ~ 10. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c 11. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b 12. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 -13. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/063fc99f-a4b2-4187-a065-0fdd287ebee2 -14. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 -15. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg +13. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 +14. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From 152c1d50ca803f8332abc29af9731acecca83a5d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 12:37:37 +0100 Subject: [PATCH 160/338] Include filenames in buffer context selection, improve buffers context Signed-off-by: Tomas Slusny --- README.md | 11 +++-------- lua/CopilotChat/config.lua | 40 ++++++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1ca4b265..3133d992 100644 --- a/README.md +++ b/README.md @@ -180,11 +180,11 @@ You can set the context in the prompt by using `#` followed by the context name. If context supports input, you can set the input in the prompt by using `:` followed by the input (or pressing `complete` key after `:`). Default contexts are: -- `buffer` - Includes only the current buffer in chat context. Supports input. -- `buffers` - Includes all open buffers in chat context +- `buffer` - Includes specified buffer in chat context (default current). Supports input. +- `buffers` - Includes all buffers in chat context (default listed). Supports input. - `file` - Includes content of provided file in chat context. Supports input. - `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. -- `git` - Includes current git diff in chat context. Supports input. +- `git` - Includes current git diff in chat context (default unstaged). Supports input. ### API @@ -297,23 +297,18 @@ Also see [here](/lua/CopilotChat/config.lua): -- default contexts contexts = { buffer = { - description = 'Includes only the current buffer in chat context. Supports input.', -- see config.lua for implementation }, buffers = { - description = 'Includes all open buffers in chat context.', -- see config.lua for implementation }, file = { - description = 'Includes content of provided file in chat context. Supports input.', -- see config.lua for implementation }, files = { - description = 'Includes all non-hidden filenames in the current workspace in chat context. Supports input.', -- see config.lua for implementation }, git = { - description = 'Includes current git diff in chat context. Supports input.', -- see config.lua for implementation }, }, diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 7f364cf8..79c250c5 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -136,11 +136,27 @@ return { -- default contexts contexts = { buffer = { - description = 'Includes only the current buffer in chat context. Supports input.', + description = 'Includes specified buffer in chat context (default current). Supports input.', input = function(callback) - vim.ui.select(vim.api.nvim_list_bufs(), { - prompt = 'Select a buffer> ', - }, callback) + vim.ui.select( + vim.tbl_map( + function(buf) + return { id = buf, name = vim.api.nvim_buf_get_name(buf) } + end, + vim.tbl_filter(function(buf) + return vim.api.nvim_buf_is_loaded(buf) and vim.fn.buflisted(buf) == 1 + end, vim.api.nvim_list_bufs()) + ), + { + prompt = 'Select a buffer> ', + format_item = function(item) + return item.name + end, + }, + function(choice) + callback(choice and choice.id) + end + ) end, resolve = function(input, source) return { @@ -149,12 +165,20 @@ return { end, }, buffers = { - description = 'Includes all open buffers in chat context.', - resolve = function() + description = 'Includes all buffers in chat context (default listed). Supports input.', + input = function(callback) + vim.ui.select({ 'listed', 'visible' }, { + prompt = 'Select buffer scope> ', + }, callback) + end, + resolve = function(input) + input = input or 'listed' return vim.tbl_map( context.outline, vim.tbl_filter(function(b) - return vim.api.nvim_buf_is_loaded(b) and vim.fn.buflisted(b) == 1 + return vim.api.nvim_buf_is_loaded(b) + and vim.fn.buflisted(b) == 1 + and (input == 'listed' or #vim.fn.win_findbuf(b) > 0) end, vim.api.nvim_list_bufs()) ) end, @@ -189,7 +213,7 @@ return { end, }, git = { - description = 'Includes current git diff in chat context. Supports input.', + description = 'Includes current git diff in chat context (default unstaged). Supports input.', input = function(callback) vim.ui.select({ 'unstaged', 'staged' }, { prompt = 'Select diff type> ', From 742767ecbf65a80290e6b14c889dd569b3be1feb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Nov 2024 11:40:17 +0000 Subject: [PATCH 161/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 0b8070a7..e8529db7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -224,11 +224,11 @@ in the prompt by using `#` followed by the context name. If context supports input, you can set the input in the prompt by using `:` followed by the input (or pressing `complete` key after `:`). Default contexts are: -- `buffer` - Includes only the current buffer in chat context. Supports input. -- `buffers` - Includes all open buffers in chat context +- `buffer` - Includes specified buffer in chat context (default current). Supports input. +- `buffers` - Includes all buffers in chat context (default listed). Supports input. - `file` - Includes content of provided file in chat context. Supports input. - `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. -- `git` - Includes current git diff in chat context. Supports input. +- `git` - Includes current git diff in chat context (default unstaged). Supports input. API ~ @@ -344,23 +344,18 @@ Also see here : -- default contexts contexts = { buffer = { - description = 'Includes only the current buffer in chat context. Supports input.', -- see config.lua for implementation }, buffers = { - description = 'Includes all open buffers in chat context.', -- see config.lua for implementation }, file = { - description = 'Includes content of provided file in chat context. Supports input.', -- see config.lua for implementation }, files = { - description = 'Includes all non-hidden filenames in the current workspace in chat context. Supports input.', -- see config.lua for implementation }, git = { - description = 'Includes current git diff in chat context. Supports input.', -- see config.lua for implementation }, }, From 70ccd4de6ebc97f9a651f17d78b1df50fd8127fa Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 13:26:02 +0100 Subject: [PATCH 162/338] Properly parse context inputs with `:` in them Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9341da11..540d70f2 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -562,8 +562,8 @@ function M.ask(prompt, config) local embeddings = {} local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') - local context_name = split[1] - local context_input = split[2] + local context_name = table.remove(split, 1) + local context_input = table.concat(split, ':') local context_value = config.contexts[context_name] if context_value then From d177720ad3a0880f3b077c6fffd3dc0975363862 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 13:56:02 +0100 Subject: [PATCH 163/338] refactor: improve semantic embeddings performance Previously the semantic embeddings were using older model and not using the query in most optimal way. Now it is using text-embedding-3-small model for reduced cost and better performance, embeddings are stored in 512 dimensions and the embedding process was refactored to be more straightforward and use less API calls by including the query in the initial embedding call. Also slightly improves the formatting of the embedding prompt to be more readable. Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 52 +++++++++---------------------------- lua/CopilotChat/copilot.lua | 7 ++--- lua/CopilotChat/init.lua | 17 ++++++------ 3 files changed, 24 insertions(+), 52 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 3330939e..37e27db5 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -41,7 +41,7 @@ local off_side_rule_languages = { } local big_file_threshold = 500 -local selection_threshold = 200 +local multi_file_threshold = 3 local function spatial_distance_cosine(a, b) local dot_product = 0 @@ -264,58 +264,30 @@ function M.gitdiff(type, bufnr) } end ----@class CopilotChat.context.find_for_query.opts ----@field embeddings table ----@field prompt string ----@field selection string? ----@field filename string ----@field filetype string - --- Filter embeddings based on the query ---@param copilot CopilotChat.Copilot ----@param opts CopilotChat.context.find_for_query.opts +---@param embeddings table ---@return table -function M.filter_embeddings(copilot, opts) - local embeddings = opts.embeddings - local prompt = opts.prompt - local selection = opts.selection - local filename = opts.filename - local filetype = opts.filetype +function M.filter_embeddings(copilot, embeddings) + -- If there is only query embedding or we are under the threshold, return embeddings without query + if #embeddings <= (1 + multi_file_threshold) then + table.remove(embeddings, 1) + return embeddings + end local out = copilot:embed(embeddings) - if #out == 0 then + if #out <= 1 then return {} end - -- If selection is too big, truncate it - if selection then - local lines = vim.split(selection, '\n') - selection = #lines > selection_threshold - and table.concat(vim.list_slice(lines, 1, selection_threshold), '\n') - or selection - end - log.debug(string.format('Got %s embeddings', #out)) - local query_out = copilot:embed({ - { - prompt = prompt, - content = selection, - filename = filename, - filetype = filetype, - }, - }) - - local query = query_out[1] - if not query then - return {} - end + local query = table.remove(out, 1) + log.debug('Query Prompt:', query.prompt) local data = data_ranked_by_relatedness(query, out, 20) - - log.debug('Prompt:', query.prompt) - log.debug('Content:', query.content) log.debug('Ranked data:', #data) + for i, item in ipairs(data) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 7d4254b9..ad61afe1 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -315,6 +315,7 @@ end local function generate_embedding_request(inputs, model) return { + dimensions = 512, input = vim.tbl_map(function(input) local out = '' if input.prompt then @@ -323,8 +324,8 @@ local function generate_embedding_request(inputs, model) if input.content then out = out .. string.format( - '# FILE:%s CONTEXT\n```%s\n%s\n```', - input.filename:upper(), + 'File: `%s`\n```%s\n%s\n```', + input.filename, input.filetype, input.content ) @@ -881,7 +882,7 @@ end ---@param opts CopilotChat.copilot.embed.opts: Options for the request function Copilot:embed(inputs, opts) opts = opts or {} - local model = opts.model or 'copilot-text-embedding-ada-002' + local model = opts.model or 'text-embedding-3-small' local chunk_size = opts.chunk_size or 15 if not inputs or #inputs == 0 then diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 540d70f2..7f339857 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -559,7 +559,12 @@ function M.ask(prompt, config) )) or 'untitled' - local embeddings = {} + local embeddings = { + { + prompt = prompt, + }, + } + local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') local context_name = table.remove(split, 1) @@ -604,14 +609,8 @@ function M.ask(prompt, config) end end - local query_ok, filtered_embeddings = pcall(context.filter_embeddings, state.copilot, { - embeddings = embeddings, - prompt = prompt, - selection = selection.lines, - filename = filename, - filetype = filetype, - bufnr = state.source.bufnr, - }) + local query_ok, filtered_embeddings = + pcall(context.filter_embeddings, state.copilot, embeddings) if not query_ok then vim.schedule(function() From a2934a16d0a19129f64a2f6ecc344b7adedd9f03 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 14:26:07 +0100 Subject: [PATCH 164/338] perf: add caching for embeddings Add caching mechanism for embeddings to avoid re-computing them for the same content. This improves performance by storing and reusing previously computed embeddings based on filename and content hash. Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 16 +++++-------- lua/CopilotChat/copilot.lua | 45 +++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 37e27db5..f330eeed 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -41,7 +41,7 @@ local off_side_rule_languages = { } local big_file_threshold = 500 -local multi_file_threshold = 3 +local multi_file_threshold = 2 local function spatial_distance_cosine(a, b) local dot_product = 0 @@ -269,25 +269,19 @@ end ---@param embeddings table ---@return table function M.filter_embeddings(copilot, embeddings) - -- If there is only query embedding or we are under the threshold, return embeddings without query + -- If we dont need to embed anything, just return the embeddings without query if #embeddings <= (1 + multi_file_threshold) then table.remove(embeddings, 1) return embeddings end + -- Get embeddings local out = copilot:embed(embeddings) - if #out <= 1 then - return {} - end - log.debug(string.format('Got %s embeddings', #out)) - local query = table.remove(out, 1) - log.debug('Query Prompt:', query.prompt) - - local data = data_ranked_by_relatedness(query, out, 20) + -- Rate embeddings by relatedness to the query + local data = data_ranked_by_relatedness(table.remove(out, 1), out, 20) log.debug('Ranked data:', #data) - for i, item in ipairs(data) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index ad61afe1..3b202f4f 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -104,6 +104,10 @@ local function machine_id() return hex end +local function quick_hash(str) + return #str .. str:sub(1, 32) .. str:sub(-32) +end + local function find_config_path() local config = vim.fn.expand('$XDG_CONFIG_HOME') if config and vim.fn.isdirectory(config) > 0 then @@ -346,6 +350,7 @@ end local Copilot = class(function(self, proxy, allow_insecure) self.history = {} + self.embedding_cache = {} self.github_token = nil self.token = nil self.sessionid = nil @@ -881,18 +886,34 @@ end ---@param inputs table: The inputs to embed ---@param opts CopilotChat.copilot.embed.opts: Options for the request function Copilot:embed(inputs, opts) - opts = opts or {} - local model = opts.model or 'text-embedding-3-small' - local chunk_size = opts.chunk_size or 15 - if not inputs or #inputs == 0 then return {} end + -- Check which embeddings need to be fetched + local cached_embeddings = {} + local uncached_embeddings = {} + for _, embed in ipairs(inputs) do + if embed.content then + local key = embed.filename .. quick_hash(embed.content) + if self.embedding_cache[key] then + table.insert(cached_embeddings, self.embedding_cache[key]) + else + table.insert(uncached_embeddings, embed) + end + else + table.insert(uncached_embeddings, embed) + end + end + + opts = opts or {} + local model = opts.model or 'text-embedding-3-small' + local chunk_size = opts.chunk_size or 15 + local out = {} - for i = 1, #inputs, chunk_size do - local chunk = vim.list_slice(inputs, i, i + chunk_size - 1) + for i = 1, #uncached_embeddings, chunk_size do + local chunk = vim.list_slice(uncached_embeddings, i, i + chunk_size - 1) local body = vim.json.encode(generate_embedding_request(chunk, model)) local response, err = curl_post( 'https://api.githubcopilot.com/embeddings', @@ -934,7 +955,16 @@ function Copilot:embed(inputs, opts) end end - return out + -- Cache embeddings + for _, embedding in ipairs(out) do + if embedding.content then + local key = embedding.filename .. quick_hash(embedding.content) + self.embedding_cache[key] = embedding + end + end + + -- Merge cached embeddings and newly fetched embeddings and return + return vim.list_extend(out, cached_embeddings) end --- Stop the running job @@ -951,6 +981,7 @@ end function Copilot:reset() local stopped = self:stop() self.history = {} + self.embedding_cache = {} return stopped end From 50fa71eef19598447d754f62deb7e3be105ecc80 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 15:33:33 +0100 Subject: [PATCH 165/338] docs: improve README with better examples and cleaner structure (#516) * docs: improve README with better examples and cleaner structure Previously, the documentation for custom prompts, system prompts and contexts was scattered and duplicated across multiple sections. This change consolidates and improves the documentation by: - Moving examples next to their relevant sections - Adding clearer and more concise examples for custom prompts, system prompts and contexts - Removing duplicate content that was spread across multiple sections - Making github/copilot.vim the primary recommended Copilot backend Signed-off-by: Tomas Slusny * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Signed-off-by: Tomas Slusny Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 126 +++++++++++++++++++-------------------- lua/CopilotChat/init.lua | 3 + 2 files changed, 66 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 3133d992..5ec7c886 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ return { "CopilotC-Nvim/CopilotChat.nvim", branch = "canary", dependencies = { - { "zbirenbaum/copilot.lua" }, -- or github/copilot.vim + { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim" }, -- for curl, log wrapper }, build = "make tiktoken", -- Only on MacOS or Linux @@ -52,7 +52,7 @@ Similar to the lazy setup, you can use the following configuration: ```vim call plug#begin() -Plug 'zbirenbaum/copilot.lua' +Plug 'github/copilot.vim' Plug 'nvim-lua/plenary.nvim' Plug 'CopilotC-Nvim/CopilotChat.nvim', { 'branch': 'canary' } call plug#end() @@ -72,7 +72,7 @@ EOF mkdir -p ~/.config/nvim/pack/copilotchat/start cd ~/.config/nvim/pack/copilotchat/start -git clone https://github.com/zbirenbaum/copilot.lua +git clone https://github.com/github/copilot.vim git clone https://github.com/nvim-lua/plenary.nvim git clone -b canary https://github.com/CopilotC-Nvim/CopilotChat.nvim @@ -121,6 +121,21 @@ Default prompts are: - `Tests` - Please generate tests for my code - `Commit` - Write commit message for the change with commitizen convention +You can define custom prompts like this (only `prompt` is required): + +```lua +{ + prompts = { + MyCustomPrompt = { + prompt = 'Explain how it works.', + system_prompt = 'You are very good at explaining stuff', + mapping = 'ccmc', + description = 'My custom prompt description', + } + } +} +``` + ### System Prompts System prompts specify the behavior of the AI model. You can reference system prompts with `/PROMPT_NAME` in chat. @@ -131,6 +146,18 @@ Default system prompts are: - `COPILOT_REVIEW` - On top of the base instructions adds code review behavior with instructions on how to generate diagnostics - `COPILOT_GENERATE` - On top of the base instructions adds code generation behavior, with predefined formatting and generation rules +You can define custom system prompts like this (works same as `prompts` so you can combine prompt and system prompt definitions): + +```lua +{ + prompts = { + Yarrr = { + system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', + } + } +} +``` + ### Sticky Prompts You can set sticky prompt in chat by prefixing the text with `> ` using markdown blockquote syntax. @@ -186,6 +213,39 @@ Default contexts are: - `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. - `git` - Includes current git diff in chat context (default unstaged). Supports input. +You can define custom contexts like this: + +```lua +{ + contexts = { + birthday = { + input = function(callback) + vim.ui.select({ 'user', 'napoleon' }, { + prompt = 'Select birthday> ', + }, callback) + end, + resolve = function(input) + input = input or 'user' + local birthday = input + if input == 'user' then + birthday = birthday .. ' birthday is April 1, 1990' + elseif input == 'napoleon' then + birthday = birthday .. ' birthday is August 15, 1769' + end + + return { + { + content = birthday, + filename = input .. '_birthday', + filetype = 'text', + } + } + end + } + } +} +``` + ### API ```lua @@ -396,66 +456,6 @@ Also see [here](/lua/CopilotChat/config.lua): } ``` -For further reference, you can view @jellydn's [configuration](https://github.com/jellydn/lazy-nvim-ide/blob/main/lua/plugins/extras/copilot-chat-v2.lua). - -### Defining a prompt with command and keymap - -This will define prompt that you can reference with `/MyCustomPrompt` in chat, call with `:CopilotChatMyCustomPrompt` or use the keymap `ccmc`. -It will use visual selection as default selection. If you are using `lazy.nvim` and are already lazy loading based on `Commands` make sure to include the prompt -commands and keymaps in `cmd` and `keys` respectively. - -```lua -{ - prompts = { - MyCustomPrompt = { - prompt = 'Explain how it works.', - mapping = 'ccmc', - description = 'My custom prompt description', - selection = require('CopilotChat.select').visual, - }, - }, -} -``` - -### Referencing system or user prompts - -You can reference system or user prompts in your configuration or in chat with `/PROMPT_NAME` slash notation. -For collection of default `COPILOT_` (system) and `USER_` (user) prompts, see [here](/lua/CopilotChat/prompts.lua). - -```lua -{ - prompts = { - MyCustomPrompt = { - prompt = '/COPILOT_EXPLAIN Explain how it works.', - }, - MyCustomPrompt2 = { - prompt = '/MyCustomPrompt Include some additional context.', - }, - }, -} -``` - -### Custom system prompts - -You can define custom system prompts by using `system_prompt` property when passing config around. - -```lua -{ - system_prompt = 'Your name is Github Copilot and you are a AI assistant for developers.', - prompts = { - Johnny = { - system_prompt = 'Your name is Johny Microsoft and you are not an AI assistant for developers.', - prompt = 'Explain how it works.', - }, - Yarrr = { - system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.' - }, - }, -} -``` - -To use any of your custom prompts, simply do `:CopilotChat`. E.g. `:CopilotChatJohnny` or `:CopilotChatYarrr What is a sorting algo?`. Tab autocomplete will help you out. - ### Customizing buffers You can set local options for the buffers that are created by this plugin: `copilot-diff`, `copilot-system-prompt`, `copilot-user-selection`, `copilot-chat`. diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7f339857..0bcfe735 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -570,6 +570,9 @@ function M.ask(prompt, config) local context_name = table.remove(split, 1) local context_input = table.concat(split, ':') local context_value = config.contexts[context_name] + if vim.trim(context_input) == '' then + context_input = nil + end if context_value then for _, embedding in ipairs(context_value.resolve(context_input, state.source)) do From 5236f0eea0a0c7520defbb20d6b926ef2be58d27 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Nov 2024 14:33:54 +0000 Subject: [PATCH 166/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 150 +++++++++++++++++++++----------------------- 1 file changed, 70 insertions(+), 80 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e8529db7..be534a58 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -54,7 +54,7 @@ LAZY.NVIM ~ "CopilotC-Nvim/CopilotChat.nvim", branch = "canary", dependencies = { - { "zbirenbaum/copilot.lua" }, -- or github/copilot.vim + { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim" }, -- for curl, log wrapper }, build = "make tiktoken", -- Only on MacOS or Linux @@ -76,7 +76,7 @@ Similar to the lazy setup, you can use the following configuration: >vim call plug#begin() - Plug 'zbirenbaum/copilot.lua' + Plug 'github/copilot.vim' Plug 'nvim-lua/plenary.nvim' Plug 'CopilotC-Nvim/CopilotChat.nvim', { 'branch': 'canary' } call plug#end() @@ -97,7 +97,7 @@ MANUAL ~ mkdir -p ~/.config/nvim/pack/copilotchat/start cd ~/.config/nvim/pack/copilotchat/start - git clone https://github.com/zbirenbaum/copilot.lua + git clone https://github.com/github/copilot.vim git clone https://github.com/nvim-lua/plenary.nvim git clone -b canary https://github.com/CopilotC-Nvim/CopilotChat.nvim @@ -153,6 +153,21 @@ Default prompts are: - `Tests` - Please generate tests for my code - `Commit` - Write commit message for the change with commitizen convention +You can define custom prompts like this (only `prompt` is required): + +>lua + { + prompts = { + MyCustomPrompt = { + prompt = 'Explain how it works.', + system_prompt = 'You are very good at explaining stuff', + mapping = 'ccmc', + description = 'My custom prompt description', + } + } + } +< + SYSTEM PROMPTS ~ @@ -164,6 +179,19 @@ prompts with `/PROMPT_NAME` in chat. Default system prompts are: - `COPILOT_REVIEW` - On top of the base instructions adds code review behavior with instructions on how to generate diagnostics - `COPILOT_GENERATE` - On top of the base instructions adds code generation behavior, with predefined formatting and generation rules +You can define custom system prompts like this (works same as `prompts` so you +can combine prompt and system prompt definitions): + +>lua + { + prompts = { + Yarrr = { + system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', + } + } + } +< + STICKY PROMPTS ~ @@ -230,6 +258,39 @@ input, you can set the input in the prompt by using `:` followed by the input - `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. - `git` - Includes current git diff in chat context (default unstaged). Supports input. +You can define custom contexts like this: + +>lua + { + contexts = { + birthday = { + input = function(callback) + vim.ui.select({ 'user', 'napoleon' }, { + prompt = 'Select birthday> ', + }, callback) + end, + resolve = function(input) + input = input or 'user' + local birthday = input + if input == 'user' then + birthday = birthday .. ' birthday is April 1, 1990' + elseif input == 'napoleon' then + birthday = birthday .. ' birthday is August 15, 1769' + end + + return { + { + content = birthday, + filename = input .. '_birthday', + filetype = 'text', + } + } + end + } + } + } +< + API ~ @@ -443,76 +504,6 @@ Also see here : } < -For further reference, you can view @jellydn’s configuration -. - - -DEFINING A PROMPT WITH COMMAND AND KEYMAP ~ - -This will define prompt that you can reference with `/MyCustomPrompt` in chat, -call with `:CopilotChatMyCustomPrompt` or use the keymap `ccmc`. It -will use visual selection as default selection. If you are using `lazy.nvim` -and are already lazy loading based on `Commands` make sure to include the -prompt commands and keymaps in `cmd` and `keys` respectively. - ->lua - { - prompts = { - MyCustomPrompt = { - prompt = 'Explain how it works.', - mapping = 'ccmc', - description = 'My custom prompt description', - selection = require('CopilotChat.select').visual, - }, - }, - } -< - - -REFERENCING SYSTEM OR USER PROMPTS ~ - -You can reference system or user prompts in your configuration or in chat with -`/PROMPT_NAME` slash notation. For collection of default `COPILOT_` (system) -and `USER_` (user) prompts, see here . - ->lua - { - prompts = { - MyCustomPrompt = { - prompt = '/COPILOT_EXPLAIN Explain how it works.', - }, - MyCustomPrompt2 = { - prompt = '/MyCustomPrompt Include some additional context.', - }, - }, - } -< - - -CUSTOM SYSTEM PROMPTS ~ - -You can define custom system prompts by using `system_prompt` property when -passing config around. - ->lua - { - system_prompt = 'Your name is Github Copilot and you are a AI assistant for developers.', - prompts = { - Johnny = { - system_prompt = 'Your name is Johny Microsoft and you are not an AI assistant for developers.', - prompt = 'Explain how it works.', - }, - Yarrr = { - system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.' - }, - }, - } -< - -To use any of your custom prompts, simply do `:CopilotChat`. E.g. -`:CopilotChatJohnny` or `:CopilotChatYarrr What is a sorting algo?`. Tab -autocomplete will help you out. - CUSTOMIZING BUFFERS ~ @@ -685,13 +676,12 @@ STARGAZERS OVER TIME ~ 5. *All Contributors*: https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat&link=%23contributors- 6. *@jellydn*: 7. *@deathbeam*: -8. *@jellydn*: -9. *Chat with buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif -10. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c -11. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b -12. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 -13. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 -14. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg +8. *Chat with buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif +9. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c +10. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b +11. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 +12. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 +13. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From 5da38541d3be861d23ce2375aa5d93d2a47ce460 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 15:55:50 +0100 Subject: [PATCH 167/338] Use relative paths for file references in contexts Makes data smaller and faster Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/context.lua | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 79c250c5..632168c5 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -141,7 +141,7 @@ return { vim.ui.select( vim.tbl_map( function(buf) - return { id = buf, name = vim.api.nvim_buf_get_name(buf) } + return { id = buf, name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ':p:.') } end, vim.tbl_filter(function(buf) return vim.api.nvim_buf_is_loaded(buf) and vim.fn.buflisted(buf) == 1 diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index f330eeed..d08e9951 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -116,7 +116,7 @@ function M.file(filename) return { content = table.concat(content, '\n'), - filename = filename, + filename = vim.fn.fnamemodify(filename, ':p:.'), filetype = vim.filetype.match({ filename = filename }), } end @@ -127,6 +127,7 @@ end ---@return CopilotChat.copilot.embed? function M.outline(bufnr) local name = vim.api.nvim_buf_get_name(bufnr) + name = vim.fn.fnamemodify(name, ':p:.') local ft = vim.bo[bufnr].filetype -- If buffer is not too big, just return the content From 7e5d6d83652153c6aaef01a64cbfafe912ba3cf9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 16:03:17 +0100 Subject: [PATCH 168/338] Explicitely mention that multiple contexts are supported in README Signed-off-by: Tomas Slusny --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ec7c886..f79ebb18 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ You can install more agents from [here](https://github.com/marketplace?type=apps ### Contexts Contexts are used to determine the context of the chat. -You can set the context in the prompt by using `#` followed by the context name. +You can add context to the prompt by using `#` followed by the context name (multiple contexts are supported). If context supports input, you can set the input in the prompt by using `:` followed by the input (or pressing `complete` key after `:`). Default contexts are: From 78901925c9742b8f56a74adcaa36ffb425ecb735 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Nov 2024 15:04:14 +0000 Subject: [PATCH 169/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index be534a58..aac36352 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -247,10 +247,11 @@ You can install more agents from here CONTEXTS ~ -Contexts are used to determine the context of the chat. You can set the context -in the prompt by using `#` followed by the context name. If context supports -input, you can set the input in the prompt by using `:` followed by the input -(or pressing `complete` key after `:`). Default contexts are: +Contexts are used to determine the context of the chat. You can add context to +the prompt by using `#` followed by the context name (multiple contexts are +supported). If context supports input, you can set the input in the prompt by +using `:` followed by the input (or pressing `complete` key after `:`). Default +contexts are: - `buffer` - Includes specified buffer in chat context (default current). Supports input. - `buffers` - Includes all buffers in chat context (default listed). Supports input. From 74e486c44d7ff651cb1a25b32417cfd08ab673a3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 16:21:55 +0100 Subject: [PATCH 170/338] Add documentation for selections to README Signed-off-by: Tomas Slusny --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index f79ebb18..5a93ebdc 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,47 @@ You can define custom contexts like this: } ``` +### Selections + +Selections are used to determine the source of the chat (so basically what to chat about). +Selections are configurable either by default or by prompt. +Default selection is `visual` or `buffer` (if no visual selection). +Default supported selections that live in `local select = require("CopilotChat.select")` are: + +- `select.visual` - Current visual selection. Works well with diffs. +- `select.buffer` - Current buffer content. Works well with diffs. +- `select.line` - Current line content. Works decently with diffs. +- `select.unnamed` - Content from the unnamed register. +- `select.clipboard` - Content from system clipboard. + +You can define custom selection functions like this: + +```lua +{ + selection = function() + -- Get content from * register + local lines = vim.fn.getreg('*') + if not lines or lines == '' then + return nil + end + + return { + lines = lines, + } + end +} +``` + +Or chain multiple selections like this: + +```lua +{ + selection = function(source) + return select.visual(source) or select.buffer(source) + end +} +``` + ### API ```lua From 9462e89d76b1b4c698a9fa2ddcc8821848cd0dff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Nov 2024 15:26:59 +0000 Subject: [PATCH 171/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index aac36352..6107a09a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -293,6 +293,49 @@ You can define custom contexts like this: < +SELECTIONS ~ + +Selections are used to determine the source of the chat (so basically what to +chat about). Selections are configurable either by default or by prompt. +Default selection is `visual` or `buffer` (if no visual selection). Default +supported selections that live in `local select = +require("CopilotChat.select")` are: + +- `select.visual` - Current visual selection. Works well with diffs. +- `select.buffer` - Current buffer content. Works well with diffs. +- `select.line` - Current line content. Works decently with diffs. +- `select.unnamed` - Content from the unnamed register. +- `select.clipboard` - Content from system clipboard. + +You can define custom selection functions like this: + +>lua + { + selection = function() + -- Get content from * register + local lines = vim.fn.getreg('*') + if not lines or lines == '' then + return nil + end + + return { + lines = lines, + } + end + } +< + +Or chain multiple selections like this: + +>lua + { + selection = function(source) + return select.visual(source) or select.buffer(source) + end + } +< + + API ~ >lua From 0cc9b06c00d76062004e73395fb422bac85a24a4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 16:31:09 +0100 Subject: [PATCH 172/338] Add table of contents to README because of all new config sections Signed-off-by: Tomas Slusny --- README.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5a93ebdc..43e0ccb4 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,38 @@ > [!NOTE] > Plugin was rewritten to Lua from Python. Please check the [migration guide from version 1 to version 2](/MIGRATION.md) for more information. +- [Prerequisites](#prerequisites) +- [Installation](#installation) + - [Lazy.nvim](#lazy.nvim) + - [Vim-Plug](#vim-plug) + - [Manual](#manual) + - [Post-Installation](#post-installation) +- [Usage](#usage) + - [Commands](#commands) + - [Prompts](#prompts) + - [System Prompts](#system-prompts) + - [Sticky Prompts](#sticky-prompts) + - [Models](#models) + - [Agents](#agents) + - [Contexts](#contexts) + - [Selections](#selections) + - [API](#api) +- [Configuration](#configuration) + - [Default configuration](#default-configuration) + - [Customizing buffers](#customizing-buffers) +- [Tips](#tips) +- [Roadmap (Wishlist)](#roadmap-wishlist) +- [Development](#development) +- [Contributors ✨](#contributors-) + ## Prerequisites Ensure you have the following installed: - **Neovim stable (0.9.5) or nightly**. +Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabled. + Optional: - tiktoken_core: `sudo luarocks install --lua-version 5.1 tiktoken_core`. Alternatively, download a pre-built binary from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases) @@ -88,10 +114,6 @@ require("CopilotChat").setup { See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/master/nvim/.config/nvim/lua/config/copilot.lua#L14) -### Post-Installation - -Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabled. - ## Usage ### Commands From 7d45ec9f1f74fe790af3da0c770efffc1c2846bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Nov 2024 15:34:21 +0000 Subject: [PATCH 173/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 6107a09a..5098b28f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -25,6 +25,30 @@ Table of Contents *CopilotChat-table-of-contents* [!NOTE] Plugin was rewritten to Lua from Python. Please check the migration guide from version 1 to version 2 for more information. +- |CopilotChat-prerequisites| +- |CopilotChat-installation| + - |CopilotChat-lazy.nvim| + - |CopilotChat-vim-plug| + - |CopilotChat-manual| + - |CopilotChat-post-installation| +- |CopilotChat-usage| + - |CopilotChat-commands| + - |CopilotChat-prompts| + - |CopilotChat-system-prompts| + - |CopilotChat-sticky-prompts| + - |CopilotChat-models| + - |CopilotChat-agents| + - |CopilotChat-contexts| + - |CopilotChat-selections| + - |CopilotChat-api| +- |CopilotChat-configuration| + - |CopilotChat-default-configuration| + - |CopilotChat-customizing-buffers| +- |CopilotChat-tips| +- |CopilotChat-roadmap-(wishlist)| +- |CopilotChat-development| +- |CopilotChat-contributors-✨| + PREREQUISITES *CopilotChat-prerequisites* @@ -32,6 +56,9 @@ Ensure you have the following installed: - **Neovim stable (0.9.5) or nightly**. +Verify "Copilot chat in the IDE " is +enabled. + Optional: - tiktoken_core: `sudo luarocks install --lua-version 5.1 tiktoken_core`. Alternatively, download a pre-built binary from lua-tiktoken releases @@ -115,12 +142,6 @@ See @deathbeam for configuration -POST-INSTALLATION ~ - -Verify "Copilot chat in the IDE " is -enabled. - - USAGE *CopilotChat-usage* From c292cfb45786ccf0326d6fca1620f4ef98b9ce11 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 17:12:35 +0100 Subject: [PATCH 174/338] Show errors inline in response Instead of creating new error section, show error inline in response to not waste chat space Signed-off-by: Tomas Slusny --- README.md | 4 +++- lua/CopilotChat/init.lua | 15 ++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 43e0ccb4..0b00bf46 100644 --- a/README.md +++ b/README.md @@ -645,9 +645,11 @@ require('render-markdown').setup({ file_types = { 'markdown', 'copilot-chat' }, }) --- You might also want to disable default header highlighting for copilot chat when doing this +-- You might also want to disable default header highlighting for copilot chat when doing this and set error header style and separator require('CopilotChat').setup({ highlight_headers = false, + separator = '---', + error_header = '> [!ERROR] Error', -- rest of your config }) ``` diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0bcfe735..9939a949 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -174,7 +174,7 @@ local function finish(config, message, hide_help, start_of_chat) end end -local function show_error(err, config) +local function show_error(err, config, append_newline) log.error(vim.inspect(err)) if type(err) == 'string' then @@ -185,8 +185,11 @@ local function show_error(err, config) err = vim.inspect(err) end - state.chat:append('\n\n' .. config.error_header .. config.separator .. '\n\n') - state.chat:append('```\n' .. err .. '\n```') + if append_newline then + state.chat:append('\n') + end + + state.chat:append(config.error_header .. '\n```error\n' .. err .. '\n```') finish(config) end @@ -604,6 +607,7 @@ function M.ask(prompt, config) end local models = vim.tbl_keys(state.copilot:list_models()) + local has_output = false local selected_model = config.model for model in prompt:gmatch('%$([^%s]+)') do if vim.tbl_contains(models, model) then @@ -617,7 +621,7 @@ function M.ask(prompt, config) if not query_ok then vim.schedule(function() - show_error(filtered_embeddings, config) + show_error(filtered_embeddings, config, has_output) end) return end @@ -635,13 +639,14 @@ function M.ask(prompt, config) on_progress = function(token) vim.schedule(function() state.chat:append(token) + has_output = true end) end, }) if not ask_ok then vim.schedule(function() - show_error(response, config) + show_error(response, config, has_output) end) return end From ff17f9217e844a7ac7f771c70216020c703415ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Nov 2024 16:14:21 +0000 Subject: [PATCH 175/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 5098b28f..e272f07b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -685,9 +685,11 @@ installed. file_types = { 'markdown', 'copilot-chat' }, }) - -- You might also want to disable default header highlighting for copilot chat when doing this + -- You might also want to disable default header highlighting for copilot chat when doing this and set error header style and separator require('CopilotChat').setup({ highlight_headers = false, + separator = '---', + error_header = '> [!ERROR] Error', -- rest of your config }) < From d36b868dbdadc0037a365e862fb0bc5aae236986 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Nov 2024 23:57:14 +0100 Subject: [PATCH 176/338] refactor: improve diff handling implementation Extract diff handling into separate class to reduce code duplication and improve maintainability. Add custom highlighting namespace in diff class and remove redundant code from overlay class. Add utility function for finding lines between patterns. Improve diff accepting/showing logic with proper line number handling. Move highlighting setup into dedicated diff class and make overlay class more focused. Add example for code snippet markdown links in prompts to improve clarity. Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 7 +- lua/CopilotChat/diff.lua | 105 +++++++++++++++ lua/CopilotChat/init.lua | 262 +++++++++++------------------------- lua/CopilotChat/overlay.lua | 14 +- lua/CopilotChat/prompts.lua | 2 +- lua/CopilotChat/utils.lua | 56 ++++++++ 6 files changed, 251 insertions(+), 195 deletions(-) create mode 100644 lua/CopilotChat/diff.lua diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index ba80787a..55c2bcd8 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -32,6 +32,7 @@ end local Chat = class(function(self, help, on_buf_create) self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') + self.name = 'copilot-chat' self.help = help self.on_buf_create = on_buf_create self.bufnr = nil @@ -43,12 +44,12 @@ local Chat = class(function(self, help, on_buf_create) self.highlight_headers = true self.layout = nil - vim.treesitter.language.register('markdown', 'copilot-chat') + vim.treesitter.language.register('markdown', self.name) self.buf_create = function() local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr, 'copilot-chat') - vim.bo[bufnr].filetype = 'copilot-chat' + vim.api.nvim_buf_set_name(bufnr, self.name) + vim.bo[bufnr].filetype = self.name vim.bo[bufnr].syntax = 'markdown' vim.bo[bufnr].textwidth = 0 local ok, parser = pcall(vim.treesitter.get_parser, bufnr, 'markdown') diff --git a/lua/CopilotChat/diff.lua b/lua/CopilotChat/diff.lua new file mode 100644 index 00000000..560f1090 --- /dev/null +++ b/lua/CopilotChat/diff.lua @@ -0,0 +1,105 @@ +---@class CopilotChat.Diff +---@field bufnr number +---@field show fun(self: CopilotChat.Diff, change: string, reference: string, start_line: number?, end_line: number?, filetype: string, winnr: number) +---@field restore fun(self: CopilotChat.Diff, winnr: number, bufnr: number) +---@field delete fun(self: CopilotChat.Diff) +---@field get_diff fun(self: CopilotChat.Diff): string, string, number?, number? + +local Overlay = require('CopilotChat.overlay') +local utils = require('CopilotChat.utils') +local class = utils.class + +local Diff = class(function(self, help, on_buf_create) + self.hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights') + vim.api.nvim_set_hl( + self.hl_ns, + '@diff.plus', + { bg = utils.blend_color_with_neovim_bg('DiffAdd', 20) } + ) + vim.api.nvim_set_hl( + self.hl_ns, + '@diff.minus', + { bg = utils.blend_color_with_neovim_bg('DiffDelete', 20) } + ) + vim.api.nvim_set_hl( + self.hl_ns, + '@diff.delta', + { bg = utils.blend_color_with_neovim_bg('DiffChange', 20) } + ) + + self.name = 'copilot-diff' + self.help = help + self.on_buf_create = on_buf_create + self.bufnr = nil + self.change = nil + self.reference = nil + self.start_line = nil + self.end_line = nil + self.filetype = nil + + self.buf_create = function() + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, self.name) + vim.bo[bufnr].filetype = self.name + return bufnr + end +end, Overlay) + +function Diff:show(change, reference, start_line, end_line, filetype, winnr) + self.change = change + self.reference = reference + self.start_line = start_line + self.end_line = end_line + self.filetype = filetype + + self:validate() + vim.api.nvim_win_set_hl_ns(winnr, self.hl_ns) + + -- mini.diff integration (unfinished) + -- local diff_found, diff = pcall(require, 'mini.diff') + -- if diff_found then + -- diff.disable(self.bufnr) + -- Overlay.show(self, change, filetype, winnr) + -- + -- vim.b[self.bufnr].minidiff_config = { + -- source = { + -- name = self.name, + -- attach = function(bufnr) + -- diff.set_ref_text(bufnr, reference) + -- diff.toggle_overlay(bufnr) + -- end, + -- }, + -- } + -- + -- diff.enable(self.bufnr) + -- return + -- end + + Overlay.show( + self, + tostring(vim.diff(reference, change, { + result_type = 'unified', + ignore_blank_lines = true, + ignore_whitespace = true, + ignore_whitespace_change = true, + ignore_whitespace_change_at_eol = true, + ignore_cr_at_eol = true, + algorithm = 'myers', + ctxlen = #reference, + })), + filetype, + winnr, + 'diff' + ) +end + +function Diff:restore(winnr, bufnr) + Overlay.restore(self, winnr, bufnr) + vim.api.nvim_win_set_hl_ns(winnr, 0) +end + +function Diff:get_diff() + return self.change, self.reference, self.start_line, self.end_line, self.filetype +end + +return Diff diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9939a949..b0e2e9f7 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -3,6 +3,7 @@ local async = require('plenary.async') local log = require('plenary.log') local Copilot = require('CopilotChat.copilot') local Chat = require('CopilotChat.chat') +local Diff = require('CopilotChat.diff') local Overlay = require('CopilotChat.overlay') local context = require('CopilotChat.context') local prompts = require('CopilotChat.prompts') @@ -20,8 +21,7 @@ local plugin_name = 'CopilotChat.nvim' --- @field last_system_prompt string? --- @field last_prompt string? --- @field last_response string? ---- @field last_code_output string? ---- @field diff CopilotChat.Overlay? +--- @field diff CopilotChat.Diff? --- @field system_prompt CopilotChat.Overlay? --- @field user_selection CopilotChat.Overlay? --- @field help CopilotChat.Overlay? @@ -35,7 +35,6 @@ local state = { last_system_prompt = nil, last_prompt = nil, last_response = nil, - last_code_output = nil, -- Overlays diff = nil, @@ -44,62 +43,6 @@ local state = { help = nil, } -local function find_lines_between_separator( - lines, - current_line, - start_pattern, - end_pattern, - allow_end_of_file -) - if not end_pattern then - end_pattern = start_pattern - end - - local line_count = #lines - local separator_line_start = 1 - local separator_line_finish = line_count - local found_one = false - - -- Find starting separator line - for i = current_line, 1, -1 do - local line = lines[i] - - if line and string.match(line, start_pattern) then - separator_line_start = i + 1 - - for x = separator_line_start, line_count do - local next_line = lines[x] - if next_line and string.match(next_line, end_pattern) then - separator_line_finish = x - 1 - found_one = true - break - end - if allow_end_of_file and x == line_count then - separator_line_finish = x - found_one = true - break - end - end - - if found_one then - break - end - end - end - - if not found_one then - return {}, 1, 1 - end - - -- Extract everything between the last and next separator or end of file - local result = {} - for i = separator_line_start, separator_line_finish do - table.insert(result, lines[i]) - end - - return result, separator_line_start, separator_line_finish -end - local function update_prompts(prompt, system_prompt) local prompts_to_use = M.prompts() local try_again = false @@ -143,6 +86,46 @@ local function get_selection() return {} end +local function get_diff() + local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) + local current_line = vim.api.nvim_win_get_cursor(0)[1] + local section, section_start = + utils.find_lines(chat_lines, current_line, M.config.separator .. '$') + local change, change_start = + utils.find_lines(section, current_line - section_start, '^```%w+$', '^```$') + local selection = get_selection() + + local reference = nil + local start_line = nil + local end_line = nil + local filetype = nil + + if selection then + reference = selection.lines + start_line = selection.start_row + end_line = selection.end_row + filetype = selection.filetype + end + + if #change > 0 then + local header = section[change_start - 2] + if header then + local header_start_line, header_end_line = header:match('%[file:.+%]%(.+%) line:(%d+)-(%d+)') + if header_start_line and header_end_line then + start_line = tonumber(header_start_line) or 1 + end_line = tonumber(header_end_line) or start_line + reference = table.concat( + vim.api.nvim_buf_get_lines(state.source.bufnr, start_line - 1, end_line, false), + '\n' + ) + end + end + end + + filetype = filetype or vim.bo[state.source.bufnr].filetype or 'text' + return table.concat(change, '\n'), reference, start_line, end_line, filetype +end + local function finish(config, message, hide_help, start_of_chat) if not start_of_chat then state.chat:append('\n\n') @@ -538,7 +521,7 @@ function M.ask(prompt, config) -- Clear the current input prompt before asking a new question local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) local _, start_line, end_line = - find_lines_between_separator(chat_lines, #chat_lines, M.config.separator .. '$', nil, true) + utils.find_lines(chat_lines, #chat_lines, M.config.separator .. '$', nil, true) if #chat_lines == end_line then vim.api.nvim_buf_set_lines(state.chat.bufnr, start_line, end_line, false, { '' }) end @@ -571,14 +554,13 @@ function M.ask(prompt, config) local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') local context_name = table.remove(split, 1) - local context_input = table.concat(split, ':') + local context_input = vim.trim(table.concat(split, ':')) local context_value = config.contexts[context_name] - if vim.trim(context_input) == '' then - context_input = nil - end if context_value then - for _, embedding in ipairs(context_value.resolve(context_input, state.source)) do + for _, embedding in + ipairs(context_value.resolve(context_input == '' and nil or context_input, state.source)) + do if embedding then table.insert(embeddings, embedding) end @@ -690,7 +672,6 @@ function M.stop(reset, config) state.last_system_prompt = nil state.last_prompt = nil state.last_response = nil - state.last_code_output = nil end finish(config, nil, nil, reset) @@ -819,18 +800,6 @@ function M.setup(config) M.log_level(M.config.log_level) end - local hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights') - vim.api.nvim_set_hl(hl_ns, '@diff.plus', { bg = utils.blend_color_with_neovim_bg('DiffAdd', 20) }) - vim.api.nvim_set_hl( - hl_ns, - '@diff.minus', - { bg = utils.blend_color_with_neovim_bg('DiffDelete', 20) } - ) - vim.api.nvim_set_hl( - hl_ns, - '@diff.delta', - { bg = utils.blend_color_with_neovim_bg('DiffChange', 20) } - ) vim.api.nvim_set_hl(0, 'CopilotChatSpinner', { link = 'CursorColumn', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) @@ -854,40 +823,31 @@ function M.setup(config) if state.diff then state.diff:delete() end - state.diff = Overlay('copilot-diff', hl_ns, diff_help, function(bufnr) + state.diff = Diff(diff_help, function(bufnr) map_key(M.config.mappings.close, bufnr, function() state.diff:restore(state.chat.winnr, state.chat.bufnr) end) map_key(M.config.mappings.accept_diff, bufnr, function() - local current = state.last_code_output - if not current then - return - end - - local selection = get_selection() - if not selection.start_row or not selection.end_row then + local change, _, start_line, end_line = state.diff:get_diff() + if not start_line or not end_line or vim.trim(change) == '' then return end - local lines = vim.split(current, '\n') - if #lines > 0 then - vim.api.nvim_buf_set_text( - state.source.bufnr, - selection.start_row - 1, - selection.start_col - 1, - selection.end_row - 1, - selection.end_col, - lines - ) - end + vim.api.nvim_buf_set_lines( + state.source.bufnr, + start_line - 1, + end_line, + false, + vim.split(change, '\n') + ) end) end) if state.system_prompt then state.system_prompt:delete() end - state.system_prompt = Overlay('copilot-system-prompt', hl_ns, overlay_help, function(bufnr) + state.system_prompt = Overlay('copilot-system-prompt', overlay_help, function(bufnr) map_key(M.config.mappings.close, bufnr, function() state.system_prompt:restore(state.chat.winnr, state.chat.bufnr) end) @@ -896,7 +856,7 @@ function M.setup(config) if state.user_selection then state.user_selection:delete() end - state.user_selection = Overlay('copilot-user-selection', hl_ns, overlay_help, function(bufnr) + state.user_selection = Overlay('copilot-user-selection', overlay_help, function(bufnr) map_key(M.config.mappings.close, bufnr, function() state.user_selection:restore(state.chat.winnr, state.chat.bufnr) end) @@ -905,7 +865,7 @@ function M.setup(config) if state.help then state.help:delete() end - state.help = Overlay('copilot-help', hl_ns, overlay_help, function(bufnr) + state.help = Overlay('copilot-help', overlay_help, function(bufnr) map_key(M.config.mappings.close, bufnr, function() state.help:restore(state.chat.winnr, state.chat.bufnr) end) @@ -945,7 +905,7 @@ function M.setup(config) end end chat_help = chat_help .. M.config.separator .. '\n' - state.help:show(chat_help, 'markdown', 'markdown', state.chat.winnr) + state.help:show(chat_help, 'markdown', state.chat.winnr) end) map_key(M.config.mappings.reset, bufnr, M.reset) @@ -971,13 +931,8 @@ function M.setup(config) map_key(M.config.mappings.submit_prompt, bufnr, function() local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local current_line = vim.api.nvim_win_get_cursor(0)[1] - local lines = find_lines_between_separator( - chat_lines, - current_line, - M.config.separator .. '$', - nil, - true - ) + local lines = + utils.find_lines(chat_lines, current_line, M.config.separator .. '$', nil, true) M.ask(vim.trim(table.concat(lines, '\n')), state.config) end) @@ -993,7 +948,7 @@ function M.setup(config) local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local _, start_line, end_line = - find_lines_between_separator(chat_lines, cur_line, M.config.separator .. '$', nil, true) + utils.find_lines(chat_lines, cur_line, M.config.separator .. '$', nil, true) if vim.startswith(current_line, '> ') then return @@ -1025,93 +980,36 @@ function M.setup(config) end) map_key(M.config.mappings.accept_diff, bufnr, function() - local selection = get_selection() - if not selection or not selection.start_row or not selection.end_row then + local change, _, start_line, end_line = get_diff() + if not start_line or not end_line or vim.trim(change) == '' then return end - local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local current_line = vim.api.nvim_win_get_cursor(0)[1] - local section_lines, start_line = - find_lines_between_separator(chat_lines, current_line, M.config.separator .. '$') - local lines = find_lines_between_separator( - section_lines, - current_line - start_line - 1, - '^```%w+$', - '^```$' + vim.api.nvim_buf_set_lines( + state.source.bufnr, + start_line - 1, + end_line, + false, + vim.split(change, '\n') ) - if #lines > 0 then - vim.api.nvim_buf_set_text( - state.source.bufnr, - selection.start_row - 1, - selection.start_col - 1, - selection.end_row - 1, - selection.end_col, - lines - ) - end end) map_key(M.config.mappings.yank_diff, bufnr, function() - local selection = get_selection() - if not selection or not selection.lines then + local change = get_diff() + if vim.trim(change) == '' then return end - local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local current_line = vim.api.nvim_win_get_cursor(0)[1] - local section_lines, start_line = - find_lines_between_separator(chat_lines, current_line, M.config.separator .. '$') - local lines = find_lines_between_separator( - section_lines, - current_line - start_line - 1, - '^```%w+$', - '^```$' - ) - if #lines > 0 then - local content = table.concat(lines, '\n') - vim.fn.setreg(M.config.mappings.yank_diff.register, content) - end + vim.fn.setreg(M.config.mappings.yank_diff.register, change) end) map_key(M.config.mappings.show_diff, bufnr, function() - local selection = get_selection() - if not selection or not selection.lines then + local change, reference, start_line, end_line, filetype = get_diff() + if vim.trim(change) == '' or not reference then return end - local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) - local current_line = vim.api.nvim_win_get_cursor(0)[1] - local section_lines, start_line = - find_lines_between_separator(chat_lines, current_line, M.config.separator .. '$') - local lines = table.concat( - find_lines_between_separator( - section_lines, - current_line - start_line - 1, - '^```%w+$', - '^```$' - ), - '\n' - ) - if vim.trim(lines) ~= '' then - state.last_code_output = lines - - local filetype = selection.filetype or vim.bo[state.source.bufnr].filetype - - local diff = tostring(vim.diff(selection.lines, lines, { - result_type = 'unified', - ignore_blank_lines = true, - ignore_whitespace = true, - ignore_whitespace_change = true, - ignore_whitespace_change_at_eol = true, - ignore_cr_at_eol = true, - algorithm = 'myers', - ctxlen = #selection.lines, - })) - - diff = diff .. '\n' .. M.config.separator .. '\n' - state.diff:show(diff, filetype, 'diff', state.chat.winnr) - end + state.diff:show(change, reference, start_line, end_line, filetype, state.chat.winnr) end) map_key(M.config.mappings.show_system_prompt, bufnr, function() @@ -1121,7 +1019,7 @@ function M.setup(config) end prompt = prompt .. '\n' .. M.config.separator .. '\n' - state.system_prompt:show(prompt, 'markdown', 'markdown', state.chat.winnr) + state.system_prompt:show(prompt, 'markdown', state.chat.winnr) end) map_key(M.config.mappings.show_user_selection, bufnr, function() @@ -1137,7 +1035,7 @@ function M.setup(config) end lines = lines .. '\n' .. M.config.separator .. '\n' - state.user_selection:show(lines, filetype, filetype, state.chat.winnr) + state.user_selection:show(lines, filetype, state.chat.winnr) end) vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index 94d86021..7b26de70 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -2,7 +2,7 @@ ---@field bufnr number ---@field valid fun(self: CopilotChat.Overlay) ---@field validate fun(self: CopilotChat.Overlay) ----@field show fun(self: CopilotChat.Overlay, text: string, filetype: string, syntax: string, winnr: number) +---@field show fun(self: CopilotChat.Overlay, text: string, filetype: string, winnr: number) ---@field restore fun(self: CopilotChat.Overlay, winnr: number, bufnr: number) ---@field delete fun(self: CopilotChat.Overlay) ---@field show_help fun(self: CopilotChat.Overlay, msg: string, offset: number) @@ -10,8 +10,7 @@ local utils = require('CopilotChat.utils') local class = utils.class -local Overlay = class(function(self, name, hl_ns, help, on_buf_create) - self.hl_ns = hl_ns +local Overlay = class(function(self, name, help, on_buf_create) self.help = help self.on_buf_create = on_buf_create self.bufnr = nil @@ -39,19 +38,18 @@ function Overlay:validate() self.on_buf_create(self.bufnr) end -function Overlay:show(text, filetype, syntax, winnr) +function Overlay:show(text, filetype, winnr, syntax) self:validate() - vim.api.nvim_win_set_buf(winnr, self.bufnr) - vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, vim.split(text, '\n')) vim.bo[self.bufnr].modifiable = false self:show_help(self.help, -1) vim.api.nvim_win_set_cursor(winnr, { vim.api.nvim_buf_line_count(self.bufnr), 0 }) + syntax = syntax or filetype + -- Dual mode with treesitter (for diffs for example) - vim.api.nvim_win_set_hl_ns(winnr, self.hl_ns) local ok, parser = pcall(vim.treesitter.get_parser, self.bufnr, syntax) if ok and parser then vim.treesitter.start(self.bufnr, syntax) @@ -62,9 +60,7 @@ function Overlay:show(text, filetype, syntax, winnr) end function Overlay:restore(winnr, bufnr) - self.current = nil vim.api.nvim_win_set_buf(winnr, bufnr or 0) - vim.api.nvim_win_set_hl_ns(winnr, 0) end function Overlay:delete() diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index e9884695..db858129 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -80,7 +80,7 @@ Your task is to modify the provided code according to the user's request. Follow 8. If the response do not fits in a single message, split the response into multiple messages. -9. Above every returned code snippet, add `[file: ]() line:-` +9. Directly above every returned code snippet, add `[file:]() line:-`. Example: `[file:copilot.lua](nvim/.config/nvim/lua/config/copilot.lua) line:1-98`. This is markdown link syntax, so make sure to follow it. Remember that Your response SHOULD CONTAIN ONLY THE MODIFIED CODE to be used as DIRECT REPLACEMENT to the original file. ]] diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 85a1f776..bc7e2480 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -90,6 +90,62 @@ function M.blend_color_with_neovim_bg(color_name, blend) return string.format('#%02x%02x%02x', r, g, b) end +--- Find lines between two patterns +---@param lines table The lines to search +---@param current_line number The current line +---@param start_pattern string The start pattern +---@param end_pattern string? The end pattern +---@param allow_end_of_file boolean? Allow end of file as end pattern +function M.find_lines(lines, current_line, start_pattern, end_pattern, allow_end_of_file) + if not end_pattern then + end_pattern = start_pattern + end + + local line_count = #lines + local separator_line_start = 1 + local separator_line_finish = line_count + local found_one = false + + -- Find starting separator line + for i = current_line, 1, -1 do + local line = lines[i] + + if line and string.match(line, start_pattern) then + separator_line_start = i + 1 + + for x = separator_line_start, line_count do + local next_line = lines[x] + if next_line and string.match(next_line, end_pattern) then + separator_line_finish = x - 1 + found_one = true + break + end + if allow_end_of_file and x == line_count then + separator_line_finish = x + found_one = true + break + end + end + + if found_one then + break + end + end + end + + if not found_one then + return {}, 1, 1 + end + + -- Extract everything between the last and next separator or end of file + local result = {} + for i = separator_line_start, separator_line_finish do + table.insert(result, lines[i]) + end + + return result, separator_line_start, separator_line_finish +end + --- Return to normal mode function M.return_to_normal_mode() local mode = vim.fn.mode():lower() From 7d578b8698e74363216754d37346f00b63948fae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:34:58 +0800 Subject: [PATCH 177/338] [pre-commit.ci] pre-commit autoupdate (#522) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ebed8be..41862ceb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,6 @@ repos: hooks: - id: prettier - repo: https://github.com/JohnnyMorganz/StyLua - rev: v0.20.0 + rev: v2.0.1 hooks: - id: stylua # or stylua-system / stylua-github From 947ca3d906a98ffdf9928d2e2ef8c7d694d2116f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Nov 2024 00:35:18 +0000 Subject: [PATCH 178/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e272f07b..7cad79b3 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 18 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 19 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 9ceeba5bcb731193a71bdb9132892e53fbacf173 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 02:28:18 +0100 Subject: [PATCH 179/338] refactor(context): improve accuracy of buffer processing Rather than using harsh big file threshold for converting files/buffers to outline format, outline is now used mostly for similarity scoring. This preserves the original content while improving the accuracy of content matching. Additionally: - Move find_config_path to utils module for reuse - Add buffer function to get full buffer content - Rewrite outline function to work with strings rather than buffers Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 4 +- lua/CopilotChat/context.lua | 213 ++++++++++++++++++++-------------- lua/CopilotChat/copilot.lua | 20 +--- lua/CopilotChat/debuginfo.lua | 12 +- lua/CopilotChat/init.lua | 41 ++++--- lua/CopilotChat/utils.lua | 20 ++++ 6 files changed, 180 insertions(+), 130 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 632168c5..f2afddee 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -160,7 +160,7 @@ return { end, resolve = function(input, source) return { - context.outline(input and tonumber(input) or source.bufnr), + context.buffer(input and tonumber(input) or source.bufnr), } end, }, @@ -174,7 +174,7 @@ return { resolve = function(input) input = input or 'listed' return vim.tbl_map( - context.outline, + context.buffer, vim.tbl_filter(function(b) return vim.api.nvim_buf_is_loaded(b) and vim.fn.buflisted(b) == 1 diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index d08e9951..9c7a527a 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -40,7 +40,6 @@ local off_side_rule_languages = { 'fsharp', } -local big_file_threshold = 500 local multi_file_threshold = 2 local function spatial_distance_cosine(a, b) @@ -73,83 +72,32 @@ local function data_ranked_by_relatedness(query, data, top_n) return result end ---- Get list of all files in workspace ----@param pattern string? ----@return table -function M.files(pattern) - local files = vim.tbl_filter(function(file) - return vim.fn.isdirectory(file) == 0 - end, vim.fn.glob(pattern or '**/*', false, true)) - - if #files == 0 then - return {} - end - - local out = {} - - -- Create embeddings in chunks - local chunk_size = 100 - for i = 1, #files, chunk_size do - local chunk = {} - for j = i, math.min(i + chunk_size - 1, #files) do - table.insert(chunk, files[j]) - end - - table.insert(out, { - content = table.concat(chunk, '\n'), - filename = 'file_map', - filetype = 'text', - }) - end - - return out -end - ---- Get the content of a file ----@param filename string ----@return CopilotChat.copilot.embed? -function M.file(filename) - local content = vim.fn.readfile(filename) - if #content == 0 then - return - end - - return { - content = table.concat(content, '\n'), - filename = vim.fn.fnamemodify(filename, ':p:.'), - filetype = vim.filetype.match({ filename = filename }), - } -end - ---- Build an outline for a buffer +--- Build an outline from a string --- FIXME: Handle multiline function argument definitions when building the outline ----@param bufnr number ----@return CopilotChat.copilot.embed? -function M.outline(bufnr) - local name = vim.api.nvim_buf_get_name(bufnr) - name = vim.fn.fnamemodify(name, ':p:.') - local ft = vim.bo[bufnr].filetype - - -- If buffer is not too big, just return the content - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - if #lines < big_file_threshold then - return { - content = table.concat(lines, '\n'), - filename = name, - filetype = ft, - } - end +---@param content string +---@param name string +---@param ft string? +---@return CopilotChat.copilot.embed +function M.outline(content, name, ft) + ft = ft or 'text' + local lines = vim.split(content, '\n') + + local base_output = { + content = content, + filename = name, + filetype = ft, + } local lang = vim.treesitter.language.get_lang(ft) local ok, parser = false, nil if lang then - ok, parser = pcall(vim.treesitter.get_parser, bufnr, lang) + ok, parser = pcall(vim.treesitter.get_string_parser, content, lang) end if not ok or not parser then ft = string.gsub(ft, 'react', '') - ok, parser = pcall(vim.treesitter.get_parser, bufnr, ft) + ok, parser = pcall(vim.treesitter.get_string_parser, content, ft) if not ok or not parser then - return + return base_output end end @@ -178,9 +126,8 @@ function M.outline(bufnr) comment_lines = {} end - local start_line = vim.api.nvim_buf_get_lines(bufnr, start_row, start_row + 1, false)[1] - local signature_start = - vim.api.nvim_buf_get_text(bufnr, start_row, start_col, start_row, #start_line, {})[1] + local start_line = lines[start_row + 1] + local signature_start = start_line:sub(start_col + 1) table.insert(outline_lines, string.rep(' ', depth) .. vim.trim(signature_start)) -- If the function definition spans multiple lines, add an ellipsis @@ -191,7 +138,7 @@ function M.outline(bufnr) end elseif is_comment then skip_inner = true - local comment = vim.split(vim.treesitter.get_node_text(node, bufnr, {}), '\n') + local comment = vim.split(vim.treesitter.get_node_text(node, content), '\n') for _, line in ipairs(comment) do table.insert(comment_lines, vim.trim(line)) end @@ -207,8 +154,8 @@ function M.outline(bufnr) if is_outline then if not skip_inner and not vim.tbl_contains(off_side_rule_languages, ft) then - local signature_end = - vim.trim(vim.api.nvim_buf_get_text(bufnr, end_row, 0, end_row, end_col, {})[1]) + local end_line = lines[end_row + 1] + local signature_end = vim.trim(end_line:sub(1, end_col)) table.insert(outline_lines, string.rep(' ', depth) .. signature_end) end depth = depth - 1 @@ -216,21 +163,86 @@ function M.outline(bufnr) end get_outline_lines(root) - local content = table.concat(outline_lines, '\n') - if content == '' then - return + local result_content = table.concat(outline_lines, '\n') + if result_content == '' then + return base_output end return { - content = table.concat(outline_lines, '\n'), + content = result_content, filename = name, filetype = ft, } end +--- Get list of all files in workspace +---@param pattern string? +---@return table +function M.files(pattern) + local files = vim.tbl_filter(function(file) + return vim.fn.isdirectory(file) == 0 + end, vim.fn.glob(pattern or '**/*', false, true)) + + if #files == 0 then + return {} + end + + local out = {} + + -- Create embeddings in chunks + local chunk_size = 100 + for i = 1, #files, chunk_size do + local chunk = {} + for j = i, math.min(i + chunk_size - 1, #files) do + table.insert(chunk, files[j]) + end + + table.insert(out, { + content = table.concat(chunk, '\n'), + filename = 'file_map_' .. tostring(i), + filetype = 'text', + }) + end + + return out +end + +--- Get the content of a file +---@param filename string +---@return CopilotChat.copilot.embed? +function M.file(filename) + local content = vim.fn.readfile(filename) + if not content or #content == 0 then + return nil + end + + return { + content = table.concat(content, '\n'), + filename = vim.fn.fnamemodify(filename, ':p:.'), + filetype = vim.filetype.match({ filename = filename }), + } +end + +--- Get the content of a buffer +---@param bufnr number +---@return CopilotChat.copilot.embed? +function M.buffer(bufnr) + local content = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + if not content or #content == 0 then + return nil + end + + return { + content = table.concat(content, '\n'), + filename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ':p:.'), + filetype = vim.bo[bufnr].filetype, + } +end + --- Get current git diff ---@param type string? ---@param bufnr number +---@return CopilotChat.copilot.embed? function M.gitdiff(type, bufnr) type = type or 'unstaged' local bufname = vim.api.nvim_buf_get_name(bufnr) @@ -267,27 +279,48 @@ end --- Filter embeddings based on the query ---@param copilot CopilotChat.Copilot +---@param prompt string ---@param embeddings table ---@return table -function M.filter_embeddings(copilot, embeddings) - -- If we dont need to embed anything, just return the embeddings without query - if #embeddings <= (1 + multi_file_threshold) then - table.remove(embeddings, 1) +function M.filter_embeddings(copilot, prompt, embeddings) + -- If we dont need to embed anything, just return directly + if #embeddings <= multi_file_threshold then return embeddings end - -- Get embeddings - local out = copilot:embed(embeddings) - log.debug(string.format('Got %s embeddings', #out)) + table.insert(embeddings, 1, { + prompt = prompt, + filename = 'prompt', + }) + + -- Get embeddings from outlines + local embedded_data = copilot:embed(vim.tbl_map(function(embed) + if embed.content then + return M.outline(embed.content, embed.filename, embed.filetype) + end + + return embed + end, embeddings)) -- Rate embeddings by relatedness to the query - local data = data_ranked_by_relatedness(table.remove(out, 1), out, 20) - log.debug('Ranked data:', #data) - for i, item in ipairs(data) do + local ranked_data = data_ranked_by_relatedness(table.remove(embedded_data, 1), out, 20) + log.debug('Ranked data:', #ranked_data) + for i, item in ipairs(ranked_data) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end - return data + -- Return original content but keep the ranking order and scores + local result = {} + for _, ranked_item in ipairs(ranked_data) do + for _, original in ipairs(embeddings) do + if original.filename == ranked_item.filename then + table.insert(result, original) + break + end + end + end + + return result end return M diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 3b202f4f..97de0c5f 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -108,24 +108,6 @@ local function quick_hash(str) return #str .. str:sub(1, 32) .. str:sub(-32) end -local function find_config_path() - local config = vim.fn.expand('$XDG_CONFIG_HOME') - if config and vim.fn.isdirectory(config) > 0 then - return config - end - if vim.fn.has('win32') > 0 then - config = vim.fn.expand('$LOCALAPPDATA') - if not config or vim.fn.isdirectory(config) == 0 then - config = vim.fn.expand('$HOME/AppData/Local') - end - else - config = vim.fn.expand('$HOME/.config') - end - if config and vim.fn.isdirectory(config) > 0 then - return config - end -end - local function get_cached_token() -- loading token from the environment only in GitHub Codespaces local token = os.getenv('GITHUB_TOKEN') @@ -135,7 +117,7 @@ local function get_cached_token() end -- loading token from the file - local config_path = find_config_path() + local config_path = utils.config_path() if not config_path then return nil end diff --git a/lua/CopilotChat/debuginfo.lua b/lua/CopilotChat/debuginfo.lua index ec10daad..30827773 100644 --- a/lua/CopilotChat/debuginfo.lua +++ b/lua/CopilotChat/debuginfo.lua @@ -10,15 +10,19 @@ function M.open() 'Log file path:', '`' .. log.logfile .. '`', '', - 'Temp directory:', - '`' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`', - '', 'Data directory:', '`' .. vim.fn.stdpath('data') .. '`', '', + 'Config directory:', + '`' .. utils.config_path() .. '`', + '', + 'Temp directory:', + '`' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`', + '', } - local outline = context.outline(vim.api.nvim_get_current_buf()) + local buf = context.buffer(vim.api.nvim_get_current_buf()) + local outline = buf and context.outline(buf.content, buf.filename, buf.filetype) if outline then table.insert(lines, 'Current buffer outline:') table.insert(lines, '`' .. outline.filename .. '`') diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index b0e2e9f7..25593cc8 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -529,11 +529,18 @@ function M.ask(prompt, config) state.chat:append(prompt) state.chat:append('\n\n' .. config.answer_header .. config.separator .. '\n\n') + -- Resolve prompt references local system_prompt, updated_prompt = update_prompts(prompt or '', config.system_prompt) state.last_system_prompt = system_prompt state.last_prompt = prompt - prompt = updated_prompt - prompt = string.gsub(prompt, '(^|\n)>%s+', '%1') + + -- Remove sticky prefix + prompt = table.concat( + vim.tbl_map(function(l) + return l:gsub('>%s+', '') + end, vim.split(updated_prompt, '\n')), + '\n' + ) local selection = get_selection() local filetype = selection.filetype @@ -545,24 +552,20 @@ function M.ask(prompt, config) )) or 'untitled' - local embeddings = { - { - prompt = prompt, - }, - } - + local embedding_map = {} local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') local context_name = table.remove(split, 1) local context_input = vim.trim(table.concat(split, ':')) local context_value = config.contexts[context_name] + if context_input == '' then + context_input = nil + end if context_value then - for _, embedding in - ipairs(context_value.resolve(context_input == '' and nil or context_input, state.source)) - do + for _, embedding in ipairs(context_value.resolve(context_input, state.source)) do if embedding then - table.insert(embeddings, embedding) + embedding_map[embedding.filename] = embedding end end @@ -570,13 +573,21 @@ function M.ask(prompt, config) end end + -- Sort and parse contexts + local contexts = {} if config.context then - parse_context(config.context) + table.insert(contexts, config.context) end - for prompt_context in prompt:gmatch('#([^%s]+)') do + table.insert(contexts, prompt_context) + end + table.sort(contexts, function(a, b) + return #a > #b + end) + for _, prompt_context in ipairs(contexts) do parse_context(prompt_context) end + local embeddings = vim.tbl_values(embedding_map) async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) @@ -599,7 +610,7 @@ function M.ask(prompt, config) end local query_ok, filtered_embeddings = - pcall(context.filter_embeddings, state.copilot, embeddings) + pcall(context.filter_embeddings, state.copilot, prompt, embeddings) if not query_ok then vim.schedule(function() diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index bc7e2480..3df0a01b 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -49,6 +49,26 @@ function M.temp_file(text) return temp_file end +--- Finds the path to the user's config directory +---@return string? +function M.config_path() + local config = vim.fn.expand('$XDG_CONFIG_HOME') + if config and vim.fn.isdirectory(config) > 0 then + return config + end + if vim.fn.has('win32') > 0 then + config = vim.fn.expand('$LOCALAPPDATA') + if not config or vim.fn.isdirectory(config) == 0 then + config = vim.fn.expand('$HOME/AppData/Local') + end + else + config = vim.fn.expand('$HOME/.config') + end + if config and vim.fn.isdirectory(config) > 0 then + return config + end +end + --- Check if a table is equal to another table ---@param a table The first table ---@param b table The second table From d1f6ef1a390d91aed754d8d9dd152785f43c2f75 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 11:49:56 +0100 Subject: [PATCH 180/338] Add deprecate warning for selection.gitdiff Signed-off-by: Tomas Slusny --- lua/CopilotChat/select.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index e6f2c6f0..22bea447 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -1,3 +1,5 @@ +local utils = require('CopilotChat.utils') + local M = {} local function get_diagnostics_in_range(bufnr, start_line, end_line) @@ -171,4 +173,9 @@ function M.clipboard() } end +function M.gitdiff() + utils.deprecated('selection.gitdiff', 'context.gitdiff') + return nil +end + return M From a64dcd221861e4df4684baf3b3c68868b564a384 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 12:46:40 +0100 Subject: [PATCH 181/338] Standardize context embedding format Introduce raw filetype for somethign that should just be embedded directly (like prompts). Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 7 ++++--- lua/CopilotChat/copilot.lua | 10 ++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 9c7a527a..f832d316 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -289,13 +289,14 @@ function M.filter_embeddings(copilot, prompt, embeddings) end table.insert(embeddings, 1, { - prompt = prompt, + content = prompt, filename = 'prompt', + filetype = 'raw', }) -- Get embeddings from outlines local embedded_data = copilot:embed(vim.tbl_map(function(embed) - if embed.content then + if embed.filetype ~= 'raw' then return M.outline(embed.content, embed.filename, embed.filetype) end @@ -303,7 +304,7 @@ function M.filter_embeddings(copilot, prompt, embeddings) end, embeddings)) -- Rate embeddings by relatedness to the query - local ranked_data = data_ranked_by_relatedness(table.remove(embedded_data, 1), out, 20) + local ranked_data = data_ranked_by_relatedness(table.remove(embedded_data, 1), embedded_data, 20) log.debug('Ranked data:', #ranked_data) for i, item in ipairs(ranked_data) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 97de0c5f..b8828ebe 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -1,8 +1,7 @@ ---@class CopilotChat.copilot.embed +---@field content string ---@field filename string ---@field filetype string ----@field prompt string? ----@field content string? ---@class CopilotChat.copilot.ask.opts ---@field selection CopilotChat.config.selection? @@ -304,10 +303,9 @@ local function generate_embedding_request(inputs, model) dimensions = 512, input = vim.tbl_map(function(input) local out = '' - if input.prompt then - out = input.prompt .. '\n' - end - if input.content then + if input.filetype == 'raw' then + out = input.content .. '\n' + else out = out .. string.format( 'File: `%s`\n```%s\n%s\n```', From 1141e9c73d68cd0eadf1eacdeed38a4085d9c7d5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 13:58:32 +0100 Subject: [PATCH 182/338] refactor(selection): normalize line ranges and content handling Refactors selection related code to use consistent approach for handling line ranges, content and diagnostics. Updates diff/overlay code to use proper filetype defaults, handle buffer references correctly and use proper visual selection after diff application. Main changes: - Rename row to line in all API references for consistency - Move content field from lines to content for better clarity - Add proper handling of filetype defaults and buffer references - Make selection state properly track across buffer changes - Update visual selection properly after applying diffs Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 19 ++-- lua/CopilotChat/copilot.lua | 62 ++++++------- lua/CopilotChat/diff.lua | 56 ++++------- lua/CopilotChat/init.lua | 180 +++++++++++++++++++----------------- lua/CopilotChat/overlay.lua | 3 +- lua/CopilotChat/select.lua | 64 ++++++++----- 6 files changed, 191 insertions(+), 193 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index f2afddee..24149270 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -7,22 +7,19 @@ local select = require('CopilotChat.select') --- @field winnr number ---@class CopilotChat.config.selection.diagnostic ----@field message string +---@field content string +---@field start_line number +---@field end_line number ---@field severity string ----@field start_row number ----@field start_col number ----@field end_row number ----@field end_col number ---@class CopilotChat.config.selection ----@field lines string ----@field diagnostics table? +---@field content string +---@field start_line number? +---@field end_line number? ---@field filename string? ---@field filetype string? ----@field start_row number? ----@field start_col number? ----@field end_row number? ----@field end_col number? +---@field bufnr number? +---@field diagnostics table? ---@class CopilotChat.config.context ---@field description string? diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index b8828ebe..ccf49e15 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -6,10 +6,6 @@ ---@class CopilotChat.copilot.ask.opts ---@field selection CopilotChat.config.selection? ---@field embeddings table? ----@field filename string? ----@field filetype string? ----@field start_row number? ----@field end_row number? ---@field system_prompt string? ---@field model string? ---@field agent string? @@ -141,20 +137,24 @@ local function get_cached_token() return nil end -local function generate_line_numbers(content, start_row) +local function generate_line_numbers(content, start_line) local lines = vim.split(content, '\n') local total_lines = #lines local max_length = #tostring(total_lines) for i, line in ipairs(lines) do - local formatted_line_number = string.format('%' .. max_length .. 'd', i - 1 + (start_row or 1)) + local formatted_line_number = string.format('%' .. max_length .. 'd', i - 1 + (start_line or 1)) lines[i] = formatted_line_number .. ': ' .. line end content = table.concat(lines, '\n') return content end -local function generate_selection_messages(filename, filetype, selection) - local content = selection.lines +--- Generate messages for the given selection +--- @param selection CopilotChat.config.selection +local function generate_selection_messages(selection) + local filename = selection.filename or 'unknown' + local filetype = selection.filetype or 'text' + local content = selection.content if not content or content == '' then return {} @@ -162,44 +162,35 @@ local function generate_selection_messages(filename, filetype, selection) local out = string.format('# FILE:%s CONTEXT\n', filename:upper()) out = out .. "User's active selection:\n" - if selection.start_row and selection.start_row > 0 then + if selection.start_line and selection.end_line > 0 then out = out .. string.format( 'Excerpt from %s, lines %s to %s:\n', filename, - selection.start_row, - selection.end_row + selection.start_line, + selection.end_line ) end out = out .. string.format( '```%s\n%s\n```', filetype, - generate_line_numbers(content, selection.start_row) + generate_line_numbers(content, selection.start_line) ) if selection.diagnostics then local diagnostics = {} for _, diagnostic in ipairs(selection.diagnostics) do - local start_row = diagnostic.start_row - local end_row = diagnostic.end_row - if start_row == end_row then - table.insert( - diagnostics, - string.format('%s line=%d: %s', diagnostic.severity, start_row, diagnostic.message) + table.insert( + diagnostics, + string.format( + '%s line=%d-%d: %s', + diagnostic.severity, + diagnostic.start_line, + diagnostic.end_line, + diagnostic.content ) - else - table.insert( - diagnostics, - string.format( - '%s line=%d-%d: %s', - diagnostic.severity, - start_row, - end_row, - diagnostic.message - ) - ) - end + ) end out = out @@ -214,6 +205,8 @@ local function generate_selection_messages(filename, filetype, selection) } end +--- Generate messages for the given embeddings +--- @param embeddings table local function generate_embeddings_messages(embeddings) local files = {} for _, embedding in ipairs(embeddings) do @@ -314,6 +307,7 @@ local function generate_embedding_request(inputs, model) input.content ) end + return out end, inputs), model = model, @@ -533,8 +527,6 @@ function Copilot:ask(prompt, opts) opts = opts or {} prompt = vim.trim(prompt) local embeddings = opts.embeddings or {} - local filename = opts.filename or '' - local filetype = opts.filetype or '' local selection = opts.selection or {} local system_prompt = vim.trim(opts.system_prompt or prompts.COPILOT_INSTRUCTIONS) local model = opts.model or 'gpt-4o-2024-05-13' @@ -545,11 +537,9 @@ function Copilot:ask(prompt, opts) self.current_job = job_id log.trace('System prompt: ' .. system_prompt) - log.trace('Selection: ' .. (selection.lines or '')) + log.trace('Selection: ' .. (selection.content or '')) log.debug('Prompt: ' .. prompt) log.debug('Embeddings: ' .. #embeddings) - log.debug('Filename: ' .. filename) - log.debug('Filetype: ' .. filetype) log.debug('Model: ' .. model) log.debug('Agent: ' .. agent) log.debug('Temperature: ' .. temperature) @@ -574,7 +564,7 @@ function Copilot:ask(prompt, opts) tiktoken_load(tokenizer) local generated_messages = {} - local selection_messages = generate_selection_messages(filename, filetype, selection) + local selection_messages = generate_selection_messages(selection) local embeddings_messages = generate_embeddings_messages(embeddings) local generated_tokens = 0 for _, message in ipairs(selection_messages) do diff --git a/lua/CopilotChat/diff.lua b/lua/CopilotChat/diff.lua index 560f1090..8194b3fe 100644 --- a/lua/CopilotChat/diff.lua +++ b/lua/CopilotChat/diff.lua @@ -1,9 +1,18 @@ +---@class CopilotChat.Diff.diff +---@field change string +---@field reference string +---@field filename string +---@field filetype string +---@field start_line number? +---@field end_line number? +---@field bufnr number? + ---@class CopilotChat.Diff ---@field bufnr number ----@field show fun(self: CopilotChat.Diff, change: string, reference: string, start_line: number?, end_line: number?, filetype: string, winnr: number) +---@field show fun(self: CopilotChat.Diff, diff: CopilotChat.Diff.diff, winnr: number) ---@field restore fun(self: CopilotChat.Diff, winnr: number, bufnr: number) ---@field delete fun(self: CopilotChat.Diff) ----@field get_diff fun(self: CopilotChat.Diff): string, string, number?, number? +---@field get_diff fun(self: CopilotChat.Diff): CopilotChat.Diff.diff local Overlay = require('CopilotChat.overlay') local utils = require('CopilotChat.utils') @@ -31,11 +40,7 @@ local Diff = class(function(self, help, on_buf_create) self.help = help self.on_buf_create = on_buf_create self.bufnr = nil - self.change = nil - self.reference = nil - self.start_line = nil - self.end_line = nil - self.filetype = nil + self.diff = nil self.buf_create = function() local bufnr = vim.api.nvim_create_buf(false, true) @@ -45,39 +50,14 @@ local Diff = class(function(self, help, on_buf_create) end end, Overlay) -function Diff:show(change, reference, start_line, end_line, filetype, winnr) - self.change = change - self.reference = reference - self.start_line = start_line - self.end_line = end_line - self.filetype = filetype - +function Diff:show(diff, winnr) + self.diff = diff self:validate() vim.api.nvim_win_set_hl_ns(winnr, self.hl_ns) - -- mini.diff integration (unfinished) - -- local diff_found, diff = pcall(require, 'mini.diff') - -- if diff_found then - -- diff.disable(self.bufnr) - -- Overlay.show(self, change, filetype, winnr) - -- - -- vim.b[self.bufnr].minidiff_config = { - -- source = { - -- name = self.name, - -- attach = function(bufnr) - -- diff.set_ref_text(bufnr, reference) - -- diff.toggle_overlay(bufnr) - -- end, - -- }, - -- } - -- - -- diff.enable(self.bufnr) - -- return - -- end - Overlay.show( self, - tostring(vim.diff(reference, change, { + tostring(vim.diff(diff.reference, diff.change, { result_type = 'unified', ignore_blank_lines = true, ignore_whitespace = true, @@ -85,9 +65,9 @@ function Diff:show(change, reference, start_line, end_line, filetype, winnr) ignore_whitespace_change_at_eol = true, ignore_cr_at_eol = true, algorithm = 'myers', - ctxlen = #reference, + ctxlen = #diff.reference, })), - filetype, + diff.filetype, winnr, 'diff' ) @@ -99,7 +79,7 @@ function Diff:restore(winnr, bufnr) end function Diff:get_diff() - return self.change, self.reference, self.start_line, self.end_line, self.filetype + return self.diff end return Diff diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 25593cc8..d606c653 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -15,28 +15,32 @@ local plugin_name = 'CopilotChat.nvim' --- @class CopilotChat.state --- @field copilot CopilotChat.Copilot? ---- @field chat CopilotChat.Chat? --- @field source CopilotChat.config.source? +--- @field selection CopilotChat.config.selection? --- @field config CopilotChat.config? --- @field last_system_prompt string? --- @field last_prompt string? --- @field last_response string? +--- @field chat CopilotChat.Chat? --- @field diff CopilotChat.Diff? --- @field system_prompt CopilotChat.Overlay? --- @field user_selection CopilotChat.Overlay? --- @field help CopilotChat.Overlay? local state = { copilot = nil, - chat = nil, + + -- Current state tracking source = nil, + selection = nil, config = nil, - -- State tracking + -- Last state tracking last_system_prompt = nil, last_prompt = nil, last_response = nil, -- Overlays + chat = nil, diff = nil, system_prompt = nil, user_selection = nil, @@ -72,6 +76,7 @@ local function update_prompts(prompt, system_prompt) return system_prompt, result end +---@return CopilotChat.config.selection|nil local function get_selection() local bufnr = state.source.bufnr local winnr = state.source.winnr @@ -81,11 +86,12 @@ local function get_selection() and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_win_is_valid(winnr) then - return state.config.selection(state.source) or {} + return state.config.selection(state.source) end - return {} + return nil end +---@return CopilotChat.Diff.diff|nil local function get_diff() local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) local current_line = vim.api.nvim_win_get_cursor(0)[1] @@ -93,37 +99,50 @@ local function get_diff() utils.find_lines(chat_lines, current_line, M.config.separator .. '$') local change, change_start = utils.find_lines(section, current_line - section_start, '^```%w+$', '^```$') - local selection = get_selection() - - local reference = nil - local start_line = nil - local end_line = nil - local filetype = nil - - if selection then - reference = selection.lines - start_line = selection.start_row - end_line = selection.end_row - filetype = selection.filetype + + -- If we do not have selection or change there is no diff + if not state.selection or not change or #change == 0 then + return end - if #change > 0 then + local reference = state.selection.content + local start_line = state.selection.start_line + local end_line = state.selection.end_line + local filename = state.selection.filename or 'unknown' + local filetype = state.selection.filetype or 'text' + local bufnr = state.selection.bufnr + + if bufnr then local header = section[change_start - 2] - if header then - local header_start_line, header_end_line = header:match('%[file:.+%]%(.+%) line:(%d+)-(%d+)') - if header_start_line and header_end_line then - start_line = tonumber(header_start_line) or 1 - end_line = tonumber(header_end_line) or start_line - reference = table.concat( - vim.api.nvim_buf_get_lines(state.source.bufnr, start_line - 1, end_line, false), - '\n' - ) + local header_filename, header_start_line, header_end_line = + header and header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') or nil, nil, nil + if header_filename and header_start_line and header_end_line then + header_filename = vim.fn.fnamemodify(header_filename, ':p') + header_start_line = tonumber(header_start_line) or 1 + header_end_line = tonumber(header_end_line) or header_start_line + + start_line = header_start_line + end_line = header_end_line + + if vim.fn.fnamemodify(filename, ':p') ~= header_filename then + filename = header_filename + bufnr = nil -- Disable buffer updates + else + reference = + table.concat(vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false), '\n') end end end - filetype = filetype or vim.bo[state.source.bufnr].filetype or 'text' - return table.concat(change, '\n'), reference, start_line, end_line, filetype + return { + change = table.concat(change, '\n'), + reference = reference or '', + filename = filename, + filetype = filetype or 'text', + start_line = start_line, + end_line = end_line, + bufnr = bufnr, + } end local function finish(config, message, hide_help, start_of_chat) @@ -390,19 +409,18 @@ function M.highlight_selection(clear) if clear then return end - local selection = get_selection() - if not selection.start_row or not selection.end_row then + if not state.selection or not state.selection.start_line or not state.selection.bufnr then return end + vim.api.nvim_buf_set_extmark( - state.source.bufnr, + state.selection.bufnr, selection_ns, - selection.start_row - 1, - selection.start_col - 1, + state.selection.start_line - 1, + 0, { hl_group = 'CopilotChatSelection', - end_row = selection.end_row - 1, - end_col = selection.end_col, + end_row = state.selection.end_line, strict = false, } ) @@ -413,6 +431,7 @@ end function M.open(config) -- If we are already in chat window, do nothing if state.chat:active() then + state.selection = get_selection() return end @@ -426,6 +445,7 @@ function M.open(config) } utils.return_to_normal_mode() + state.selection = get_selection() state.chat:open(config) state.chat:follow() state.chat:focus() @@ -542,16 +562,6 @@ function M.ask(prompt, config) '\n' ) - local selection = get_selection() - local filetype = selection.filetype - or (vim.api.nvim_buf_is_valid(state.source.bufnr) and vim.bo[state.source.bufnr].filetype) - or 'text' - local filename = selection.filename - or (vim.api.nvim_buf_is_valid(state.source.bufnr) and vim.api.nvim_buf_get_name( - state.source.bufnr - )) - or 'untitled' - local embedding_map = {} local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') @@ -621,10 +631,8 @@ function M.ask(prompt, config) local ask_ok, response, token_count, token_max_count = pcall(state.copilot.ask, state.copilot, prompt, { - selection = selection, + selection = state.selection, embeddings = filtered_embeddings, - filename = filename, - filetype = filetype, system_prompt = system_prompt, model = selected_model, agent = selected_agent, @@ -840,17 +848,17 @@ function M.setup(config) end) map_key(M.config.mappings.accept_diff, bufnr, function() - local change, _, start_line, end_line = state.diff:get_diff() - if not start_line or not end_line or vim.trim(change) == '' then + local diff = state.diff:get_diff() + if diff or not diff.start_line or not diff.end_line or not diff.bufnr then return end vim.api.nvim_buf_set_lines( - state.source.bufnr, - start_line - 1, - end_line, + diff.bufnr, + diff.start_line - 1, + diff.end_line, false, - vim.split(change, '\n') + vim.split(diff.change, '\n') ) end) end) @@ -991,36 +999,41 @@ function M.setup(config) end) map_key(M.config.mappings.accept_diff, bufnr, function() - local change, _, start_line, end_line = get_diff() - if not start_line or not end_line or vim.trim(change) == '' then + local diff = get_diff() + if not diff or not diff.start_line or not diff.end_line or not diff.bufnr then return end - vim.api.nvim_buf_set_lines( - state.source.bufnr, - start_line - 1, - end_line, - false, - vim.split(change, '\n') - ) + local lines = vim.split(diff.change, '\n', { trimempty = false }) + local end_pos = diff.start_line + #lines - 1 + + -- Update the source buffer with the change + vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) + + -- Update visual selection marks to the diff start/end and move cursor + vim.api.nvim_win_set_cursor(state.source.winnr, { end_pos, 0 }) + vim.api.nvim_buf_set_mark(diff.bufnr, '<', diff.start_line, 0, {}) + vim.api.nvim_buf_set_mark(diff.bufnr, '>', end_pos, 0, {}) + state.selection = get_selection() + M.highlight_selection() end) map_key(M.config.mappings.yank_diff, bufnr, function() - local change = get_diff() - if vim.trim(change) == '' then + local diff = get_diff() + if not diff then return end - vim.fn.setreg(M.config.mappings.yank_diff.register, change) + vim.fn.setreg(M.config.mappings.yank_diff.register, diff.change) end) map_key(M.config.mappings.show_diff, bufnr, function() - local change, reference, start_line, end_line, filetype = get_diff() - if vim.trim(change) == '' or not reference then + local diff = get_diff() + if not diff then return end - state.diff:show(change, reference, start_line, end_line, filetype, state.chat.winnr) + state.diff:show(diff, state.chat.winnr) end) map_key(M.config.mappings.show_system_prompt, bufnr, function() @@ -1029,31 +1042,32 @@ function M.setup(config) return end - prompt = prompt .. '\n' .. M.config.separator .. '\n' - state.system_prompt:show(prompt, 'markdown', state.chat.winnr) + state.system_prompt:show(prompt .. '\n', 'markdown', state.chat.winnr) end) map_key(M.config.mappings.show_user_selection, bufnr, function() - local selection = get_selection() - if not selection or not selection.lines then - return - end - - local filetype = selection.filetype or vim.bo[state.source.bufnr].filetype - local lines = selection.lines - if vim.trim(lines) == '' then + if not state.selection or not state.selection.content then return end - lines = lines .. '\n' .. M.config.separator .. '\n' - state.user_selection:show(lines, filetype, state.chat.winnr) + state.user_selection:show( + state.selection.content .. '\n', + state.selection.filetype, + state.chat.winnr + ) end) vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { buffer = state.chat.bufnr, callback = function(ev) + local is_enter = ev.event == 'BufEnter' + + if is_enter then + state.selection = get_selection() + end + if state.config.highlight_selection then - M.highlight_selection(ev.event == 'BufLeave') + M.highlight_selection(not is_enter) end end, }) diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index 7b26de70..c1c92128 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -2,7 +2,7 @@ ---@field bufnr number ---@field valid fun(self: CopilotChat.Overlay) ---@field validate fun(self: CopilotChat.Overlay) ----@field show fun(self: CopilotChat.Overlay, text: string, filetype: string, winnr: number) +---@field show fun(self: CopilotChat.Overlay, text: string, filetype: string?, winnr: number) ---@field restore fun(self: CopilotChat.Overlay, winnr: number, bufnr: number) ---@field delete fun(self: CopilotChat.Overlay) ---@field show_help fun(self: CopilotChat.Overlay, msg: string, offset: number) @@ -47,6 +47,7 @@ function Overlay:show(text, filetype, winnr, syntax) self:show_help(self.help, -1) vim.api.nvim_win_set_cursor(winnr, { vim.api.nvim_buf_line_count(self.bufnr), 0 }) + filetype = filetype or 'text' syntax = syntax or filetype -- Dual mode with treesitter (for diffs for example) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 22bea447..993bd356 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -2,6 +2,11 @@ local utils = require('CopilotChat.utils') local M = {} +--- Get diagnostics in a given range +--- @param bufnr number +--- @param start_line number +--- @param end_line number +--- @return table|nil local function get_diagnostics_in_range(bufnr, start_line, end_line) local diagnostics = vim.diagnostic.get(bufnr) local range_diagnostics = {} @@ -16,12 +21,10 @@ local function get_diagnostics_in_range(bufnr, start_line, end_line) local lnum = diagnostic.lnum + 1 if lnum >= start_line and lnum <= end_line then table.insert(range_diagnostics, { - message = diagnostic.message, severity = severity[diagnostic.severity], - start_row = lnum, - start_col = diagnostic.col + 1, - end_row = lnum, - end_col = diagnostic.end_col and (diagnostic.end_col + 1) or diagnostic.col + 1, + content = diagnostic.message, + start_line = lnum, + end_line = diagnostic.end_lnum and diagnostic.end_lnum + 1 or lnum, }) end end @@ -29,6 +32,14 @@ local function get_diagnostics_in_range(bufnr, start_line, end_line) return #range_diagnostics > 0 and range_diagnostics or nil end +--- Get selected lines +--- @param bufnr number +--- @param start_line number +--- @param start_col number +--- @param finish_line number +--- @param finish_col number +--- @param full_line boolean +--- @return CopilotChat.config.selection|nil local function get_selection_lines(bufnr, start_line, start_col, finish_line, finish_col, full_line) -- Exit if no actual selection if start_line == finish_line and start_col == finish_col then @@ -76,11 +87,12 @@ local function get_selection_lines(bufnr, start_line, start_col, finish_line, fi end return { - lines = lines_content, - start_row = start_line, - start_col = start_col, - end_row = finish_line, - end_col = finish_col, + content = lines_content, + filename = vim.api.nvim_buf_get_name(bufnr), + filetype = vim.bo[bufnr].filetype, + start_line = start_line, + end_line = finish_line, + bufnr = bufnr, } end @@ -109,14 +121,15 @@ function M.buffer(source) end local out = { - lines = table.concat(lines, '\n'), - start_row = 1, - start_col = 1, - end_row = #lines, - end_col = #lines[#lines], + content = table.concat(lines, '\n'), + filename = vim.api.nvim_buf_get_name(bufnr), + filetype = vim.bo[bufnr].filetype, + start_line = 1, + end_line = #lines, + bufnr = bufnr, } - out.diagnostics = get_diagnostics_in_range(bufnr, out.start_row, out.end_row) + out.diagnostics = get_diagnostics_in_range(bufnr, out.start_line, out.end_line) return out end @@ -134,14 +147,15 @@ function M.line(source) end local out = { - lines = line, - start_row = cursor[1], - start_col = 1, - end_row = cursor[1], - end_col = #line, + content = line, + filename = vim.api.nvim_buf_get_name(bufnr), + filetype = vim.bo[bufnr].filetype, + start_line = cursor[1], + end_line = cursor[1], + bufnr = bufnr, } - out.diagnostics = get_diagnostics_in_range(bufnr, out.start_row, out.end_row) + out.diagnostics = get_diagnostics_in_range(bufnr, out.start_line, out.end_line) return out end @@ -155,7 +169,8 @@ function M.unnamed() end return { - lines = lines, + content = lines, + filename = 'unnamed', } end @@ -169,7 +184,8 @@ function M.clipboard() end return { - lines = lines, + content = lines, + filename = 'clipboard', } end From dcc8f5e4dadfd2b6bb481da65a9e8060a1e0f47f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 14:26:38 +0100 Subject: [PATCH 183/338] docs: Use content instead of lines in custom selector example Follows new format correctly Signed-off-by: Tomas Slusny --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0b00bf46..b223ed9e 100644 --- a/README.md +++ b/README.md @@ -287,13 +287,13 @@ You can define custom selection functions like this: { selection = function() -- Get content from * register - local lines = vim.fn.getreg('*') - if not lines or lines == '' then + local content = vim.fn.getreg('*') + if not content or content == '' then return nil end return { - lines = lines, + content = content, } end } From 7956e04ed27c433ff57b6afea8807d5dc7747961 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Nov 2024 13:28:06 +0000 Subject: [PATCH 184/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 7cad79b3..d01135cc 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -334,13 +334,13 @@ You can define custom selection functions like this: { selection = function() -- Get content from * register - local lines = vim.fn.getreg('*') - if not lines or lines == '' then + local content = vim.fn.getreg('*') + if not content or content == '' then return nil end return { - lines = lines, + content = content, } end } From 813bb7fcf4222bf21f3631c7b17bf3c37eeefc51 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 15:05:04 +0100 Subject: [PATCH 185/338] Do not stop spinner on closing window This prevents UI issue where chat is outputting but spinner is not spinning until new text is appended. Spinner can spin in background just fine so let it. Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 55c2bcd8..d3e9bae3 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -241,23 +241,21 @@ function Chat:open(config) end function Chat:close(bufnr) - if self.spinner then - self.spinner:finish() + if not self:visible() then + return end - if self:visible() then - if self:active() then - utils.return_to_normal_mode() - end - - if self.layout == 'replace' then - self:restore(self.winnr, bufnr) - else - vim.api.nvim_win_close(self.winnr, true) - end + if self:active() then + utils.return_to_normal_mode() + end - self.winnr = nil + if self.layout == 'replace' then + self:restore(self.winnr, bufnr) + else + vim.api.nvim_win_close(self.winnr, true) end + + self.winnr = nil end function Chat:focus() From bc8a18da78a51a7d649506b0f75d3dc5ff38ffb1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 15:25:05 +0100 Subject: [PATCH 186/338] refactor: improve buffer validation and diff handling Introduce utils.buf_valid helper to streamline buffer validation across the plugin. Extract apply_diff function to remove code duplication and improve consistency of diff application. This change makes the code more maintainable and reduces potential errors in buffer handling. --- lua/CopilotChat/config.lua | 5 +-- lua/CopilotChat/init.lua | 63 ++++++++++++++++++------------------- lua/CopilotChat/overlay.lua | 4 +-- lua/CopilotChat/spinner.lua | 6 +--- lua/CopilotChat/utils.lua | 7 +++++ 5 files changed, 43 insertions(+), 42 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 24149270..5d6fc415 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -1,6 +1,7 @@ local prompts = require('CopilotChat.prompts') local context = require('CopilotChat.context') local select = require('CopilotChat.select') +local utils = require('CopilotChat.utils') --- @class CopilotChat.config.source --- @field bufnr number @@ -141,7 +142,7 @@ return { return { id = buf, name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ':p:.') } end, vim.tbl_filter(function(buf) - return vim.api.nvim_buf_is_loaded(buf) and vim.fn.buflisted(buf) == 1 + return utils.buf_valid(buf) and vim.fn.buflisted(buf) == 1 end, vim.api.nvim_list_bufs()) ), { @@ -173,7 +174,7 @@ return { return vim.tbl_map( context.buffer, vim.tbl_filter(function(b) - return vim.api.nvim_buf_is_loaded(b) + return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 and (input == 'listed' or #vim.fn.win_findbuf(b) > 0) end, vim.api.nvim_list_bufs()) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d606c653..8fae10eb 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -83,7 +83,7 @@ local function get_selection() if state.config and state.config.selection - and vim.api.nvim_buf_is_valid(bufnr) + and utils.buf_valid(bufnr) and vim.api.nvim_win_is_valid(winnr) then return state.config.selection(state.source) @@ -112,7 +112,7 @@ local function get_diff() local filetype = state.selection.filetype or 'text' local bufnr = state.selection.bufnr - if bufnr then + if bufnr and utils.buf_valid(bufnr) then local header = section[change_start - 2] local header_filename, header_start_line, header_end_line = header and header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') or nil, nil, nil @@ -132,6 +132,8 @@ local function get_diff() table.concat(vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false), '\n') end end + else + bufnr = nil end return { @@ -145,6 +147,26 @@ local function get_diff() } end +---@param diff CopilotChat.Diff.diff? +local function apply_diff(diff) + if not diff or not diff.start_line or not diff.end_line or not diff.bufnr then + return + end + + local lines = vim.split(diff.change, '\n', { trimempty = false }) + local end_pos = diff.start_line + #lines - 1 + + -- Update the source buffer with the change + vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) + + -- Update visual selection marks to the diff start/end and move cursor + vim.api.nvim_win_set_cursor(state.source.winnr, { end_pos, 0 }) + vim.api.nvim_buf_set_mark(diff.bufnr, '<', diff.start_line, 0, {}) + vim.api.nvim_buf_set_mark(diff.bufnr, '>', end_pos, 0, {}) + state.selection = get_selection() + M.highlight_selection() +end + local function finish(config, message, hide_help, start_of_chat) if not start_of_chat then state.chat:append('\n\n') @@ -409,7 +431,11 @@ function M.highlight_selection(clear) if clear then return end - if not state.selection or not state.selection.start_line or not state.selection.bufnr then + if + not state.selection + or not utils.buf_valid(state.selection.bufnr) + or not state.selection.start_line + then return end @@ -848,18 +874,7 @@ function M.setup(config) end) map_key(M.config.mappings.accept_diff, bufnr, function() - local diff = state.diff:get_diff() - if diff or not diff.start_line or not diff.end_line or not diff.bufnr then - return - end - - vim.api.nvim_buf_set_lines( - diff.bufnr, - diff.start_line - 1, - diff.end_line, - false, - vim.split(diff.change, '\n') - ) + apply_diff(state.diff:get_diff()) end) end) @@ -999,23 +1014,7 @@ function M.setup(config) end) map_key(M.config.mappings.accept_diff, bufnr, function() - local diff = get_diff() - if not diff or not diff.start_line or not diff.end_line or not diff.bufnr then - return - end - - local lines = vim.split(diff.change, '\n', { trimempty = false }) - local end_pos = diff.start_line + #lines - 1 - - -- Update the source buffer with the change - vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) - - -- Update visual selection marks to the diff start/end and move cursor - vim.api.nvim_win_set_cursor(state.source.winnr, { end_pos, 0 }) - vim.api.nvim_buf_set_mark(diff.bufnr, '<', diff.start_line, 0, {}) - vim.api.nvim_buf_set_mark(diff.bufnr, '>', end_pos, 0, {}) - state.selection = get_selection() - M.highlight_selection() + apply_diff(get_diff()) end) map_key(M.config.mappings.yank_diff, bufnr, function() diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index c1c92128..fae41044 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -24,9 +24,7 @@ local Overlay = class(function(self, name, help, on_buf_create) end) function Overlay:valid() - return self.bufnr - and vim.api.nvim_buf_is_valid(self.bufnr) - and vim.api.nvim_buf_is_loaded(self.bufnr) + return utils.buf_valid(self.bufnr) end function Overlay:validate() diff --git a/lua/CopilotChat/spinner.lua b/lua/CopilotChat/spinner.lua index 4c3f8f13..1d365c43 100644 --- a/lua/CopilotChat/spinner.lua +++ b/lua/CopilotChat/spinner.lua @@ -37,11 +37,7 @@ function Spinner:start() 0, 100, vim.schedule_wrap(function() - if - not vim.api.nvim_buf_is_valid(self.bufnr) - or not vim.api.nvim_buf_is_loaded(self.bufnr) - or not self.timer - then + if not utils.buf_valid(self.bufnr) or not self.timer then self:finish() return end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 3df0a01b..ebef3bab 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -189,4 +189,11 @@ function M.debounce(fn, delay) M.timer = vim.defer_fn(fn, delay) end +--- Check if a buffer is valid +---@param bufnr number? The buffer number +---@return boolean +function M.buf_valid(bufnr) + return bufnr and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr) or false +end + return M From 91d0a127ba3905069360e175683e4bcd9a485bd9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 16:45:44 +0100 Subject: [PATCH 187/338] refactor(select): simplify visual line selection The change removes the need to track columns in visual line selection and uses line-based selection instead. This simplification maintains the same functionality while making the code more maintainable and less error-prone. --- lua/CopilotChat/select.lua | 59 +++++++------------------------------- 1 file changed, 11 insertions(+), 48 deletions(-) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 993bd356..82402fea 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -32,51 +32,27 @@ local function get_diagnostics_in_range(bufnr, start_line, end_line) return #range_diagnostics > 0 and range_diagnostics or nil end ---- Get selected lines ---- @param bufnr number ---- @param start_line number ---- @param start_col number ---- @param finish_line number ---- @param finish_col number ---- @param full_line boolean +--- Select and process current visual selection +--- @param source CopilotChat.config.source --- @return CopilotChat.config.selection|nil -local function get_selection_lines(bufnr, start_line, start_col, finish_line, finish_col, full_line) +function M.visual(source) + local bufnr = source.bufnr + + local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '<')) + local finish_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '>')) + -- Exit if no actual selection - if start_line == finish_line and start_col == finish_col then + if start_line == finish_line then return nil end - -- Get line lengths before swapping - local function get_line_length(line) - return #vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)[1] - end - -- Swap positions if selection is backwards - if start_line > finish_line or (start_line == finish_line and start_col > finish_col) then + if start_line > finish_line then start_line, finish_line = finish_line, start_line - start_col, finish_col = finish_col, start_col end - -- Handle full line selection - if full_line then - start_col = 1 - finish_col = get_line_length(finish_line) - end - - -- Ensure columns are within valid bounds - start_col = math.max(1, math.min(start_col, get_line_length(start_line))) - finish_col = math.max(start_col, math.min(finish_col, get_line_length(finish_line))) - -- Get selected text - local ok, lines = pcall( - vim.api.nvim_buf_get_text, - bufnr, - start_line - 1, - start_col - 1, - finish_line - 1, - finish_col, - {} - ) + local ok, lines = pcall(vim.api.nvim_buf_get_lines, bufnr, start_line - 1, finish_line, false) if not ok then return nil end @@ -96,19 +72,6 @@ local function get_selection_lines(bufnr, start_line, start_col, finish_line, fi } end ---- Select and process current visual selection ---- @param source CopilotChat.config.source ---- @return CopilotChat.config.selection|nil -function M.visual(source) - local bufnr = source.bufnr - - local start_line, start_col = unpack(vim.api.nvim_buf_get_mark(bufnr, '<')) - local finish_line, finish_col = unpack(vim.api.nvim_buf_get_mark(bufnr, '>')) - start_col = start_col + 1 - finish_col = finish_col + 1 - return get_selection_lines(bufnr, start_line, start_col, finish_line, finish_col, false) -end - --- Select and process whole buffer --- @param source CopilotChat.config.source --- @return CopilotChat.config.selection|nil From 17f47a4028acbe62791c8353b265fd86dc7ab49a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 16:55:36 +0100 Subject: [PATCH 188/338] fix: add validity checks for file and buffer functions Added proper validity checks for file and buffer functions to prevent errors when accessing non-existent files or invalid buffers. This makes the error handling more robust and prevents potential crashes. Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index f832d316..69dad9aa 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -1,4 +1,5 @@ local log = require('plenary.log') +local utils = require('CopilotChat.utils') local M = {} @@ -211,6 +212,10 @@ end ---@param filename string ---@return CopilotChat.copilot.embed? function M.file(filename) + if vim.fn.filereadable(filename) ~= 1 then + return nil + end + local content = vim.fn.readfile(filename) if not content or #content == 0 then return nil @@ -227,6 +232,10 @@ end ---@param bufnr number ---@return CopilotChat.copilot.embed? function M.buffer(bufnr) + if not utils.buf_valid(bufnr) then + return nil + end + local content = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) if not content or #content == 0 then return nil @@ -244,6 +253,10 @@ end ---@param bufnr number ---@return CopilotChat.copilot.embed? function M.gitdiff(type, bufnr) + if not utils.buf_valid(bufnr) then + return nil + end + type = type or 'unstaged' local bufname = vim.api.nvim_buf_get_name(bufnr) local file_path = bufname:gsub('^%w+://', '') From 0f5c0b813e0d81a6f81da32d0e10420cb20a579c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 17:09:58 +0100 Subject: [PATCH 189/338] fix: check if in insert mode before showing completions Ensures completion menu is only shown when user is in insert mode to prevent unexpected behavior when triggering completion in other modes. --- lua/CopilotChat/init.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 8fae10eb..ed016d68 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -303,6 +303,10 @@ local function trigger_complete() end M.complete_items(function(items) + if vim.fn.mode() ~= 'i' then + return + end + vim.fn.complete( cmp_start + 1, vim.tbl_filter(function(item) From d9b160c8ab166451d830d2958127a6bbb21bb58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9opold=20Mebazaa?= Date: Tue, 19 Nov 2024 18:21:56 +0100 Subject: [PATCH 190/338] fix(select): ensure diagnostics are passed to the model when in "Visual" (#537) * fix(select): ensure diagnostics are passed to the model when in visual mode I was wondering why my Copilot Chat wasn't picking up the error messages during /fix commands, and I think that's why :) Keep up the good work! * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- lua/CopilotChat/select.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 82402fea..dbdd6cf5 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -69,6 +69,7 @@ function M.visual(source) start_line = start_line, end_line = finish_line, bufnr = bufnr, + diagnostics = get_diagnostics_in_range(bufnr, start_line, finish_line), } end From 9e47dc1b0d6d80019bfc0c6f13ca7b7c0f418b89 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 18:24:05 +0100 Subject: [PATCH 191/338] docs: add lemeb as a contributor for code (#538) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 1 + 2 files changed, 8 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 50b2deb7..a033c1df 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -284,6 +284,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/1241736?v=4", "profile": "https://github.com/taketwo", "contributions": ["code"] + }, + { + "login": "lemeb", + "name": "Léopold Mebazaa", + "avatar_url": "https://avatars.githubusercontent.com/u/7331643?v=4", + "profile": "https://github.com/lemeb", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b223ed9e..6c316045 100644 --- a/README.md +++ b/README.md @@ -737,6 +737,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Tomáš Janoušek
Tomáš Janoušek

💻 Toddneal Stallworth
Toddneal Stallworth

📖 Sergey Alexandrov
Sergey Alexandrov

💻 + Léopold Mebazaa
Léopold Mebazaa

💻 From bd83598733ea22e08b59b7a32c11b15de58b7f21 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 20:09:38 +0100 Subject: [PATCH 192/338] feat: truncate large files and improve context output This change implements better handling of large files by adding truncation logic for both files and their outlines. The main improvements include: - Add truncation for files over 2000 lines - Add truncation for outlines over 600 lines - Use relative paths for filenames for better context display - Improve context formatting with proper file links - Add default values for unknown filenames and filetypes These changes help prevent token limits being exceeded while still preserving important context for the LLM. --- lua/CopilotChat/context.lua | 18 ++++++++-- lua/CopilotChat/copilot.lua | 67 +++++++++++++++++++++++++------------ lua/CopilotChat/prompts.lua | 2 ++ lua/CopilotChat/select.lua | 6 ++-- 4 files changed, 65 insertions(+), 28 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 69dad9aa..945e0b1c 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -41,7 +41,7 @@ local off_side_rule_languages = { 'fsharp', } -local multi_file_threshold = 2 +local outline_threshold = 600 local function spatial_distance_cosine(a, b) local dot_product = 0 @@ -164,6 +164,8 @@ function M.outline(content, name, ft) end get_outline_lines(root) + + -- Concatenate the outline lines local result_content = table.concat(outline_lines, '\n') if result_content == '' then return base_output @@ -297,7 +299,7 @@ end ---@return table function M.filter_embeddings(copilot, prompt, embeddings) -- If we dont need to embed anything, just return directly - if #embeddings <= multi_file_threshold then + if #embeddings == 0 then return embeddings end @@ -310,7 +312,17 @@ function M.filter_embeddings(copilot, prompt, embeddings) -- Get embeddings from outlines local embedded_data = copilot:embed(vim.tbl_map(function(embed) if embed.filetype ~= 'raw' then - return M.outline(embed.content, embed.filename, embed.filetype) + local outline = M.outline(embed.content, embed.filename, embed.filetype) + local outline_lines = vim.split(outline.content, '\n') + + -- If outline is too big, truncate it + if #outline_lines > 0 and #outline_lines > outline_threshold then + outline_lines = vim.list_slice(outline_lines, 1, outline_threshold) + table.insert(outline_lines, '... (truncated)') + end + + outline.content = table.concat(outline_lines, '\n') + return outline end return embed diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index ccf49e15..1229d4ca 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -35,6 +35,8 @@ local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') local class = utils.class local temp_file = utils.temp_file +local context_format = '[#file:%s](#file:%s-context)\n' +local big_file_threshold = 2000 local timeout = 30000 local version_headers = { ['editor-version'] = 'Neovim/' @@ -139,12 +141,25 @@ end local function generate_line_numbers(content, start_line) local lines = vim.split(content, '\n') + local truncated = false + + -- If the file is too big, truncate it + if #lines > big_file_threshold then + lines = vim.list_slice(lines, 1, big_file_threshold) + truncated = true + end + local total_lines = #lines local max_length = #tostring(total_lines) for i, line in ipairs(lines) do local formatted_line_number = string.format('%' .. max_length .. 'd', i - 1 + (start_line or 1)) lines[i] = formatted_line_number .. ': ' .. line end + + if truncated then + table.insert(lines, '... (truncated)') + end + content = table.concat(lines, '\n') return content end @@ -194,11 +209,15 @@ local function generate_selection_messages(selection) end out = out - .. string.format('\n# FILE:%s DIAGNOSTICS:\n%s', filename, table.concat(diagnostics, '\n')) + .. string.format( + "\nDiagnostics in user's active selection:\n%s", + table.concat(diagnostics, '\n') + ) end return { { + context = string.format(context_format, filename, filename), content = out, role = 'user', }, @@ -210,7 +229,7 @@ end local function generate_embeddings_messages(embeddings) local files = {} for _, embedding in ipairs(embeddings) do - local filename = embedding.filename + local filename = embedding.filename or 'unknown' if not files[filename] then files[filename] = {} end @@ -220,11 +239,13 @@ local function generate_embeddings_messages(embeddings) local out = {} for filename, group in pairs(files) do + local filetype = group[1].filetype or 'text' table.insert(out, { + context = string.format(context_format, filename, filename), content = string.format( '# FILE:%s CONTEXT\n```%s\n%s\n```', filename:upper(), - group[1].filetype, + filetype, generate_line_numbers(table.concat( vim.tbl_map(function(e) return vim.trim(e.content) @@ -251,6 +272,7 @@ local function generate_ask_request( ) local messages = {} local system_role = stream and 'system' or 'user' + local contexts = {} if system_prompt ~= '' then table.insert(messages, { @@ -260,13 +282,25 @@ local function generate_ask_request( end for _, message in ipairs(generated_messages) do - table.insert(messages, message) + table.insert(messages, { + content = message.content, + role = message.role, + }) + + if message.context then + contexts[message.context] = true + end end for _, message in ipairs(history) do table.insert(messages, message) end + if not vim.tbl_isempty(contexts) then + log.info('Contexts found') + prompt = table.concat(vim.tbl_keys(contexts), '\n') .. '\n' .. prompt + end + table.insert(messages, { content = prompt, role = 'user', @@ -577,14 +611,10 @@ function Copilot:ask(prompt, opts) local system_tokens = tiktoken.count(system_prompt) local required_tokens = prompt_tokens + system_tokens + generated_tokens - -- Reserve space for first embedding if its smaller than half of max tokens - local reserved_tokens = 0 - if #embeddings_messages > 0 then - local file_tokens = tiktoken.count(embeddings_messages[1].content) - if file_tokens < max_tokens / 2 then - reserved_tokens = file_tokens - end - end + -- Reserve space for first embedding + local reserved_tokens = #embeddings_messages > 0 + and tiktoken.count(embeddings_messages[1].content) + or 0 -- Calculate how many tokens we can use for history local history_limit = max_tokens - required_tokens - reserved_tokens @@ -608,16 +638,6 @@ function Copilot:ask(prompt, opts) end end - -- Prepend links to embeddings to the prompt - local embeddings_prompt = '' - for _, embedding in ipairs(embeddings) do - embeddings_prompt = embeddings_prompt - .. string.format('[#file:%s](#file:%s-context)\n', embedding.filename, embedding.filename) - end - if embeddings_prompt ~= '' then - prompt = embeddings_prompt .. '\n' .. prompt - end - local last_message = nil local errored = false local finished = false @@ -864,6 +884,9 @@ function Copilot:embed(inputs, opts) local cached_embeddings = {} local uncached_embeddings = {} for _, embed in ipairs(inputs) do + embed.filename = embed.filename or 'unknown' + embed.filetype = embed.filetype or 'text' + if embed.content then local key = embed.filename .. quick_hash(embed.content) if self.embedding_cache[key] then diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index db858129..088371e0 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -82,6 +82,8 @@ Your task is to modify the provided code according to the user's request. Follow 9. Directly above every returned code snippet, add `[file:]() line:-`. Example: `[file:copilot.lua](nvim/.config/nvim/lua/config/copilot.lua) line:1-98`. This is markdown link syntax, so make sure to follow it. +10. When examining code pay close attention to diagnostics. When fixing diagnostics, add the diagnostic message as a comment next to the line where the fix was applied. + Remember that Your response SHOULD CONTAIN ONLY THE MODIFIED CODE to be used as DIRECT REPLACEMENT to the original file. ]] diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index dbdd6cf5..4b536501 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -64,7 +64,7 @@ function M.visual(source) return { content = lines_content, - filename = vim.api.nvim_buf_get_name(bufnr), + filename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ':p:.'), filetype = vim.bo[bufnr].filetype, start_line = start_line, end_line = finish_line, @@ -86,7 +86,7 @@ function M.buffer(source) local out = { content = table.concat(lines, '\n'), - filename = vim.api.nvim_buf_get_name(bufnr), + filename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ':p:.'), filetype = vim.bo[bufnr].filetype, start_line = 1, end_line = #lines, @@ -112,7 +112,7 @@ function M.line(source) local out = { content = line, - filename = vim.api.nvim_buf_get_name(bufnr), + filename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ':p:.'), filetype = vim.bo[bufnr].filetype, start_line = cursor[1], end_line = cursor[1], From a191ad38f82a12ec4e3e3ad27480c031b454ae3a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Nov 2024 19:11:14 +0000 Subject: [PATCH 193/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index d01135cc..5d496cbf 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -724,7 +724,7 @@ If you want to contribute to this project, please read the CONTRIBUTING.md Thanks goes to these wonderful people (emoji key ): -gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻thomastthai📖Tomáš Janoušek💻Toddneal Stallworth📖Sergey Alexandrov💻This project follows the all-contributors +gptlang💻 📖Dung Duc Huynh (Kaka)💻 📖Ahmed Haracic💻Trí Thiện Nguyễn💻He Zhizhou💻Guruprakash Rajakkannu💻kristofka💻PostCyberPunk📖Katsuhiko Nishimra💻Erno Hopearuoho💻Shaun Garwood💻neutrinoA4💻 📖Jack Muratore💻Adriel Velazquez💻 📖Tomas Slusny💻 📖Nisal📖Tobias Gårdhus📖Petr Dlouhý📖Dylan Madisetti💻Aaron Weisberg💻 📖Jose Tlacuilo💻 📖Kevin Traver💻 📖dTry💻Arata Furukawa💻Ling💻Ivan Frolov💻Folke Lemaitre💻 📖GitMurf💻Dmitrii Lipin💻jinzhongjia📖guill💻Sjon-Paul Brown💻Renzo Mondragón💻 📖fjchen7💻Radosław Woźniak💻JakubPecenka💻thomastthai📖Tomáš Janoušek💻Toddneal Stallworth📖Sergey Alexandrov💻Léopold Mebazaa💻This project follows the all-contributors specification. Contributions of any kind are welcome! From 0c3592cb0800dbd7984e691ed28664de919c7bbc Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 20:18:06 +0100 Subject: [PATCH 194/338] fix: make header pattern matching more robust Add fallback pattern matching for file headers without markdown link format. This ensures the plugin works correctly with both markdown and plain text file references in the header. --- lua/CopilotChat/init.lua | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ed016d68..f43487db 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -114,22 +114,30 @@ local function get_diff() if bufnr and utils.buf_valid(bufnr) then local header = section[change_start - 2] - local header_filename, header_start_line, header_end_line = - header and header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') or nil, nil, nil - if header_filename and header_start_line and header_end_line then - header_filename = vim.fn.fnamemodify(header_filename, ':p') - header_start_line = tonumber(header_start_line) or 1 - header_end_line = tonumber(header_end_line) or header_start_line - - start_line = header_start_line - end_line = header_end_line - - if vim.fn.fnamemodify(filename, ':p') ~= header_filename then - filename = header_filename - bufnr = nil -- Disable buffer updates - else - reference = - table.concat(vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false), '\n') + + if header then + local header_filename, header_start_line, header_end_line = + header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') + if not header_filename then + header_filename, header_start_line, header_end_line = + header:match('%[file:(.+)%]% line:(%d+)-(%d+)') + end + + if header_filename and header_start_line and header_end_line then + header_filename = vim.fn.fnamemodify(header_filename, ':p') + header_start_line = tonumber(header_start_line) or 1 + header_end_line = tonumber(header_end_line) or header_start_line + + start_line = header_start_line + end_line = header_end_line + + if vim.fn.fnamemodify(filename, ':p') ~= header_filename then + filename = header_filename + bufnr = nil -- Disable buffer updates + else + reference = + table.concat(vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false), '\n') + end end end else From 08d501cbf2535fe4f753f7d01e0a74a77ae991aa Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 20:42:01 +0100 Subject: [PATCH 195/338] fix: fix o1-preview non-streaming response parsing After refactor to plenary.async we accidentally lost a way to make stream and callbacks work the same. So split parsing logic to parse_line function and use it on final response as well when the response is non-streaming Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 85 ++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 1229d4ca..481838b6 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -654,29 +654,8 @@ function Copilot:ask(prompt, opts) job:shutdown(0) end - local function stream_func(err, line, job) - if not line or errored or finished then - return - end - - if self.current_job ~= job_id then - finish_stream(nil, job) - return - end - - if err or vim.startswith(line, '{"error"') then - finish_stream('Failed to get response: ' .. (err and vim.inspect(err) or line), job) - return - end - - if not vim.startswith(line, 'data: ') then - return - end - - line = line:gsub('^%s*data:%s*', ''):gsub('%s*$', '') - - if line == '[DONE]' then - finish_stream(nil, job) + local function parse_line(line) + if not line then return end @@ -688,8 +667,7 @@ function Copilot:ask(prompt, opts) }) if not ok then - finish_stream('Failed to parse response: ' .. vim.inspect(content) .. '\n' .. line, job) - return + return content end if content.copilot_references then @@ -727,6 +705,39 @@ function Copilot:ask(prompt, opts) full_response = full_response .. content end + local function stream_func(err, line, job) + if not line or errored or finished then + return + end + + if self.current_job ~= job_id then + finish_stream(nil, job) + return + end + + if err or vim.startswith(line, '{"error"') then + finish_stream('Failed to get response: ' .. (err and vim.inspect(err) or line), job) + return + end + + if not vim.startswith(line, 'data: ') then + return + end + + line = line:gsub('^%s*data:%s*', ''):gsub('%s*$', '') + + if line == '[DONE]' then + finish_stream(nil, job) + return + end + + err = parse_line(line) + if err then + finish_stream('Failed to parse response: ' .. vim.inspect(err) .. '\n' .. line, job) + end + end + + local is_stream = not vim.startswith(model, 'o1') local body = vim.json.encode( generate_ask_request( self.history, @@ -736,7 +747,7 @@ function Copilot:ask(prompt, opts) model, temperature, max_output_tokens, - not vim.startswith(model, 'o1') + is_stream ) ) @@ -749,14 +760,16 @@ function Copilot:ask(prompt, opts) url = 'https://api.githubcopilot.com/agents/' .. agent .. '?chat' end - local response, err = curl_post( - url, - vim.tbl_extend('force', self.request_args, { - headers = self:authenticate(), - body = temp_file(body), - stream = stream_func, - }) - ) + local args = vim.tbl_extend('force', self.request_args, { + headers = self:authenticate(), + body = temp_file(body), + }) + + if is_stream then + args.stream = stream_func + end + + local response, err = curl_post(url, args) if self.current_job ~= job_id then return nil, nil, nil @@ -803,6 +816,10 @@ function Copilot:ask(prompt, opts) return end + if not is_stream then + parse_line(response.body) + end + if full_response == '' then error('Failed to get response: empty response') return From d18793c243637c71d34da170e88d7be6e8a40dc8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 21:00:23 +0100 Subject: [PATCH 196/338] fix: remove unnecessary escape in pattern matching The '%' character in the pattern matching string doesn't need to be escaped as it's not a special pattern matching character in Lua. --- lua/CopilotChat/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f43487db..bdfa0252 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -120,7 +120,7 @@ local function get_diff() header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') if not header_filename then header_filename, header_start_line, header_end_line = - header:match('%[file:(.+)%]% line:(%d+)-(%d+)') + header:match('%[file:(.+)%] line:(%d+)-(%d+)') end if header_filename and header_start_line and header_end_line then From 806b20b3d4e56b22992a2f2b7892270be5ff6ef5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 21:54:49 +0100 Subject: [PATCH 197/338] refactor: improve selection handling and highlighting The selection handling logic has been reorganized to be more robust and maintainable: - Moved highlight_selection from public to local function - Added update_selection function to handle source buffer tracking - Fixed selection updates when switching between windows - Selection state is now updated based on previous window - Removed redundant selection updates from open function - Fixed buffer reference in BufEnter/BufLeave autocmds --- lua/CopilotChat/init.lua | 106 ++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index bdfa0252..151b6102 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -76,19 +76,63 @@ local function update_prompts(prompt, system_prompt) return system_prompt, result end ----@return CopilotChat.config.selection|nil -local function get_selection() - local bufnr = state.source.bufnr - local winnr = state.source.winnr +--- Updates the selection based on previous window +local function update_selection() + local prev_winnr = vim.fn.win_getid(vim.fn.winnr('#')) + if prev_winnr ~= state.chat.winnr then + state.source = { + bufnr = vim.api.nvim_win_get_buf(prev_winnr), + winnr = prev_winnr, + } + end + + local bufnr = state.source and state.source.bufnr + local winnr = state.source and state.source.winnr + if state.config and state.config.selection and utils.buf_valid(bufnr) + and winnr and vim.api.nvim_win_is_valid(winnr) then - return state.config.selection(state.source) + state.selection = state.config.selection(state.source) + else + state.selection = nil + end +end + +--- Highlights the selection in the source buffer. +---@param clear? boolean +local function highlight_selection(clear) + local selection_ns = vim.api.nvim_create_namespace('copilot-chat-selection') + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + vim.api.nvim_buf_clear_namespace(buf, selection_ns, 0, -1) end - return nil + + if clear then + return + end + + if + not state.selection + or not utils.buf_valid(state.selection.bufnr) + or not state.selection.start_line + then + return + end + + vim.api.nvim_buf_set_extmark( + state.selection.bufnr, + selection_ns, + state.selection.start_line - 1, + 0, + { + hl_group = 'CopilotChatSelection', + end_row = state.selection.end_line, + strict = false, + } + ) end ---@return CopilotChat.Diff.diff|nil @@ -171,8 +215,8 @@ local function apply_diff(diff) vim.api.nvim_win_set_cursor(state.source.winnr, { end_pos, 0 }) vim.api.nvim_buf_set_mark(diff.bufnr, '<', diff.start_line, 0, {}) vim.api.nvim_buf_set_mark(diff.bufnr, '>', end_pos, 0, {}) - state.selection = get_selection() - M.highlight_selection() + update_selection() + highlight_selection() end local function finish(config, message, hide_help, start_of_chat) @@ -433,57 +477,17 @@ function M.prompts(skip_system) return prompts_to_use end ---- Highlights the selection in the source buffer. ----@param clear? boolean -function M.highlight_selection(clear) - local selection_ns = vim.api.nvim_create_namespace('copilot-chat-selection') - for _, buf in ipairs(vim.api.nvim_list_bufs()) do - vim.api.nvim_buf_clear_namespace(buf, selection_ns, 0, -1) - end - if clear then - return - end - if - not state.selection - or not utils.buf_valid(state.selection.bufnr) - or not state.selection.start_line - then - return - end - - vim.api.nvim_buf_set_extmark( - state.selection.bufnr, - selection_ns, - state.selection.start_line - 1, - 0, - { - hl_group = 'CopilotChatSelection', - end_row = state.selection.end_line, - strict = false, - } - ) -end - --- Open the chat window. ---@param config CopilotChat.config|CopilotChat.config.prompt|nil function M.open(config) -- If we are already in chat window, do nothing if state.chat:active() then - state.selection = get_selection() return end config = vim.tbl_deep_extend('force', M.config, config or {}) state.config = config - - -- Save the source buffer and window (e.g the buffer we are currently asking about) - state.source = { - bufnr = vim.api.nvim_get_current_buf(), - winnr = vim.api.nvim_get_current_win(), - } - utils.return_to_normal_mode() - state.selection = get_selection() state.chat:open(config) state.chat:follow() state.chat:focus() @@ -1069,16 +1073,16 @@ function M.setup(config) end) vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { - buffer = state.chat.bufnr, + buffer = bufnr, callback = function(ev) local is_enter = ev.event == 'BufEnter' if is_enter then - state.selection = get_selection() + update_selection() end if state.config.highlight_selection then - M.highlight_selection(not is_enter) + highlight_selection(not is_enter) end end, }) From 9bfe2e27e558dcbd92caea8f829867927a6ebfa8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 22:23:43 +0100 Subject: [PATCH 198/338] feat: support header diff matching without selection - Use header info (filename/lines) if no selection is available - Compare header info with current buffer name to validate that the diff can be applied Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 97 ++++++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 151b6102..ac327793 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -144,54 +144,70 @@ local function get_diff() local change, change_start = utils.find_lines(section, current_line - section_start, '^```%w+$', '^```$') - -- If we do not have selection or change there is no diff - if not state.selection or not change or #change == 0 then - return + -- If no change block found, return nil + if not change or #change == 0 then + return nil end - local reference = state.selection.content - local start_line = state.selection.start_line - local end_line = state.selection.end_line - local filename = state.selection.filename or 'unknown' - local filetype = state.selection.filetype or 'text' - local bufnr = state.selection.bufnr - - if bufnr and utils.buf_valid(bufnr) then - local header = section[change_start - 2] - - if header then - local header_filename, header_start_line, header_end_line = - header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') - if not header_filename then - header_filename, header_start_line, header_end_line = - header:match('%[file:(.+)%] line:(%d+)-(%d+)') - end - - if header_filename and header_start_line and header_end_line then - header_filename = vim.fn.fnamemodify(header_filename, ':p') - header_start_line = tonumber(header_start_line) or 1 - header_end_line = tonumber(header_end_line) or header_start_line - - start_line = header_start_line - end_line = header_end_line + -- Try to get header info first + local header = section[change_start - 2] + local header_filename, header_start_line, header_end_line + if header then + header_filename, header_start_line, header_end_line = + header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') + if not header_filename then + header_filename, header_start_line, header_end_line = + header:match('%[file:(.+)%] line:(%d+)-(%d+)') + end + if header_filename then + header_filename = vim.fn.fnamemodify(header_filename, ':p') + header_start_line = tonumber(header_start_line) or 1 + header_end_line = tonumber(header_end_line) or header_start_line + end + end - if vim.fn.fnamemodify(filename, ':p') ~= header_filename then - filename = header_filename - bufnr = nil -- Disable buffer updates - else - reference = - table.concat(vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false), '\n') + -- Initialize variables with selection if available + local reference = state.selection and state.selection.content + local start_line = state.selection and state.selection.start_line + local end_line = state.selection and state.selection.end_line + local filename = state.selection and state.selection.filename + local filetype = state.selection and state.selection.filetype + local bufnr = state.selection and state.selection.bufnr + + -- If we have header info, try to use it + if header_filename then + -- Try to find matching buffer and window + if not bufnr or vim.fn.fnamemodify(filename or '', ':p') ~= header_filename then + for _, win in ipairs(vim.api.nvim_list_wins()) do + local win_buf = vim.api.nvim_win_get_buf(win) + if vim.fn.fnamemodify(vim.api.nvim_buf_get_name(win_buf), ':p') == header_filename then + bufnr = win_buf + break end end end - else - bufnr = nil + + filename = header_filename + start_line = header_start_line + end_line = header_end_line + + -- If we found a valid buffer, get the reference content + if bufnr and utils.buf_valid(bufnr) then + reference = + table.concat(vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false), '\n') + filetype = vim.bo[bufnr].filetype + end + end + + -- If we don't have either selection or valid header info, we can't proceed + if not start_line or not end_line then + return nil end return { change = table.concat(change, '\n'), reference = reference or '', - filename = filename, + filename = filename or 'unknown', filetype = filetype or 'text', start_line = start_line, end_line = end_line, @@ -205,6 +221,11 @@ local function apply_diff(diff) return end + local winnr = vim.fn.win_findbuf(diff.bufnr)[1] + if not winnr then + return + end + local lines = vim.split(diff.change, '\n', { trimempty = false }) local end_pos = diff.start_line + #lines - 1 @@ -212,7 +233,7 @@ local function apply_diff(diff) vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) -- Update visual selection marks to the diff start/end and move cursor - vim.api.nvim_win_set_cursor(state.source.winnr, { end_pos, 0 }) + vim.api.nvim_win_set_cursor(winnr, { end_pos, 0 }) vim.api.nvim_buf_set_mark(diff.bufnr, '<', diff.start_line, 0, {}) vim.api.nvim_buf_set_mark(diff.bufnr, '>', end_pos, 0, {}) update_selection() From 0e333e3d561a5d4cc8b5ed6d73fa545590ba5b98 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 22:50:21 +0100 Subject: [PATCH 199/338] feat: add jump to diff feature with `gj` mapping Adds new mapping that allows jumping to the target file and line number from the diff view. If the target file is already open in a buffer, it will switch to that buffer. Otherwise, it creates a new buffer with proper filetype. This improves navigation between diff view and source files, making it easier to review and apply suggested changes. Signed-off-by: Tomas Slusny --- README.md | 3 + lua/CopilotChat/config.lua | 4 ++ lua/CopilotChat/diff.lua | 4 +- lua/CopilotChat/init.lua | 127 ++++++++++++++++++++++++------------- lua/CopilotChat/utils.lua | 11 ++++ 5 files changed, 103 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 6c316045..94e69375 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,9 @@ Also see [here](/lua/CopilotChat/config.lua): normal = '', insert = '' }, + jump_to_diff = { + normal = 'gj', + }, yank_diff = { normal = 'gy', register = '"', diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 5d6fc415..becf0cbd 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -60,6 +60,7 @@ local utils = require('CopilotChat.utils') ---@field submit_prompt CopilotChat.config.mapping? ---@field toggle_sticky CopilotChat.config.mapping? ---@field accept_diff CopilotChat.config.mapping? +---@field jump_to_diff CopilotChat.config.mapping? ---@field yank_diff CopilotChat.config.mapping? ---@field show_diff CopilotChat.config.mapping? ---@field show_system_prompt CopilotChat.config.mapping? @@ -329,6 +330,9 @@ return { normal = '', insert = '', }, + jump_to_diff = { + normal = 'gj', + }, yank_diff = { normal = 'gy', register = '"', diff --git a/lua/CopilotChat/diff.lua b/lua/CopilotChat/diff.lua index 8194b3fe..6a39204e 100644 --- a/lua/CopilotChat/diff.lua +++ b/lua/CopilotChat/diff.lua @@ -3,8 +3,8 @@ ---@field reference string ---@field filename string ---@field filetype string ----@field start_line number? ----@field end_line number? +---@field start_line number +---@field end_line number ---@field bufnr number? ---@class CopilotChat.Diff diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ac327793..7884afc9 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -76,32 +76,6 @@ local function update_prompts(prompt, system_prompt) return system_prompt, result end ---- Updates the selection based on previous window -local function update_selection() - local prev_winnr = vim.fn.win_getid(vim.fn.winnr('#')) - if prev_winnr ~= state.chat.winnr then - state.source = { - bufnr = vim.api.nvim_win_get_buf(prev_winnr), - winnr = prev_winnr, - } - end - - local bufnr = state.source and state.source.bufnr - local winnr = state.source and state.source.winnr - - if - state.config - and state.config.selection - and utils.buf_valid(bufnr) - and winnr - and vim.api.nvim_win_is_valid(winnr) - then - state.selection = state.config.selection(state.source) - else - state.selection = nil - end -end - --- Highlights the selection in the source buffer. ---@param clear? boolean local function highlight_selection(clear) @@ -135,6 +109,34 @@ local function highlight_selection(clear) ) end +--- Updates the selection based on previous window +local function update_selection() + local prev_winnr = vim.fn.win_getid(vim.fn.winnr('#')) + if prev_winnr ~= state.chat.winnr then + state.source = { + bufnr = vim.api.nvim_win_get_buf(prev_winnr), + winnr = prev_winnr, + } + end + + local bufnr = state.source and state.source.bufnr + local winnr = state.source and state.source.winnr + + if + state.config + and state.config.selection + and utils.buf_valid(bufnr) + and winnr + and vim.api.nvim_win_is_valid(winnr) + then + state.selection = state.config.selection(state.source) + else + state.selection = nil + end + + highlight_selection() +end + ---@return CopilotChat.Diff.diff|nil local function get_diff() local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) @@ -160,7 +162,7 @@ local function get_diff() header:match('%[file:(.+)%] line:(%d+)-(%d+)') end if header_filename then - header_filename = vim.fn.fnamemodify(header_filename, ':p') + header_filename = vim.fn.fnamemodify(header_filename, ':p:.') header_start_line = tonumber(header_start_line) or 1 header_end_line = tonumber(header_end_line) or header_start_line end @@ -174,20 +176,20 @@ local function get_diff() local filetype = state.selection and state.selection.filetype local bufnr = state.selection and state.selection.bufnr - -- If we have header info, try to use it + -- If we have header info, use it as source of truth if header_filename then -- Try to find matching buffer and window - if not bufnr or vim.fn.fnamemodify(filename or '', ':p') ~= header_filename then - for _, win in ipairs(vim.api.nvim_list_wins()) do - local win_buf = vim.api.nvim_win_get_buf(win) - if vim.fn.fnamemodify(vim.api.nvim_buf_get_name(win_buf), ':p') == header_filename then - bufnr = win_buf - break - end + bufnr = nil + for _, win in ipairs(vim.api.nvim_list_wins()) do + local win_buf = vim.api.nvim_win_get_buf(win) + if utils.filename_same(vim.api.nvim_buf_get_name(win_buf), header_filename) then + bufnr = win_buf + break end end filename = header_filename + filetype = vim.filetype.match({ filename = filename }) start_line = header_start_line end_line = header_end_line @@ -217,7 +219,7 @@ end ---@param diff CopilotChat.Diff.diff? local function apply_diff(diff) - if not diff or not diff.start_line or not diff.end_line or not diff.bufnr then + if not diff or not diff.bufnr then return end @@ -227,17 +229,15 @@ local function apply_diff(diff) end local lines = vim.split(diff.change, '\n', { trimempty = false }) - local end_pos = diff.start_line + #lines - 1 -- Update the source buffer with the change vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) -- Update visual selection marks to the diff start/end and move cursor - vim.api.nvim_win_set_cursor(winnr, { end_pos, 0 }) + vim.api.nvim_win_set_cursor(winnr, { diff.start_line, 0 }) vim.api.nvim_buf_set_mark(diff.bufnr, '<', diff.start_line, 0, {}) - vim.api.nvim_buf_set_mark(diff.bufnr, '>', end_pos, 0, {}) + vim.api.nvim_buf_set_mark(diff.bufnr, '>', diff.start_line + #lines - 1, 0, {}) update_selection() - highlight_selection() end local function finish(config, message, hide_help, start_of_chat) @@ -1054,6 +1054,47 @@ function M.setup(config) apply_diff(get_diff()) end) + map_key(M.config.mappings.jump_to_diff, bufnr, function() + if + not state.source + or not state.source.winnr + or not vim.api.nvim_win_is_valid(state.source.winnr) + then + return + end + + local diff = get_diff() + if not diff then + return + end + + local diff_bufnr = diff.bufnr + + -- Try to find existing buffer first + if not diff_bufnr then + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if utils.filename_same(vim.api.nvim_buf_get_name(buf), diff.filename) then + diff_bufnr = buf + break + end + end + end + + -- Create new empty buffer if doesn't exist + if not diff_bufnr then + diff_bufnr = vim.api.nvim_create_buf(true, false) + vim.api.nvim_buf_set_name(diff_bufnr, diff.filename) + vim.bo[diff_bufnr].filetype = diff.filetype + end + + -- Open the buffer in the source window and move cursor + vim.api.nvim_win_set_buf(state.source.winnr, diff_bufnr) + vim.api.nvim_win_set_cursor(state.source.winnr, { diff.start_line, 0 }) + vim.api.nvim_buf_set_mark(diff_bufnr, '<', diff.start_line, 0, {}) + vim.api.nvim_buf_set_mark(diff_bufnr, '>', diff.end_line, 0, {}) + update_selection() + end) + map_key(M.config.mappings.yank_diff, bufnr, function() local diff = get_diff() if not diff then @@ -1100,10 +1141,8 @@ function M.setup(config) if is_enter then update_selection() - end - - if state.config.highlight_selection then - highlight_selection(not is_enter) + else + highlight_selection(true) end end, }) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index ebef3bab..e2379df0 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -196,4 +196,15 @@ function M.buf_valid(bufnr) return bufnr and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr) or false end +--- Check if file paths are the same +---@param file1 string? The first file path +---@param file2 string? The second file path +---@return boolean +function M.filename_same(file1, file2) + if not file1 or not file2 then + return false + end + return vim.fn.fnamemodify(file1, ':p') == vim.fn.fnamemodify(file2, ':p') +end + return M From c6f8f2a1e864b10fa7b425e346d97c09eb16edaa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Nov 2024 22:29:10 +0000 Subject: [PATCH 200/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 5d496cbf..9fe6b8d2 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -552,6 +552,9 @@ Also see here : normal = '', insert = '' }, + jump_to_diff = { + normal = 'gj', + }, yank_diff = { normal = 'gy', register = '"', From b82ba49a7045323e3cf0717eda0910e0abc1f42f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 23:35:43 +0100 Subject: [PATCH 201/338] fix: Set jump marks safely after diff jumping Sometimes the content might not exist (when generating new files) and this can throw error. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7884afc9..b47f0030 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1090,8 +1090,10 @@ function M.setup(config) -- Open the buffer in the source window and move cursor vim.api.nvim_win_set_buf(state.source.winnr, diff_bufnr) vim.api.nvim_win_set_cursor(state.source.winnr, { diff.start_line, 0 }) - vim.api.nvim_buf_set_mark(diff_bufnr, '<', diff.start_line, 0, {}) - vim.api.nvim_buf_set_mark(diff_bufnr, '>', diff.end_line, 0, {}) + + -- Set the marks for visual selection and update selection + pcall(vim.api.nvim_buf_set_mark, diff_bufnr, '<', diff.start_line, 0, {}) + pcall(vim.api.nvim_buf_set_mark, diff_bufnr, '>', diff.end_line, 0, {}) update_selection() end) From 8890c126e2147bac47fae830ccae389db70b3f74 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 19 Nov 2024 23:51:54 +0100 Subject: [PATCH 202/338] chore: add extra newline when showing system prompt and selection Without this its hard to see the help text at the end Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index b47f0030..eb8baf9a 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1121,7 +1121,7 @@ function M.setup(config) return end - state.system_prompt:show(prompt .. '\n', 'markdown', state.chat.winnr) + state.system_prompt:show(prompt .. '\n\n', 'markdown', state.chat.winnr) end) map_key(M.config.mappings.show_user_selection, bufnr, function() @@ -1130,7 +1130,7 @@ function M.setup(config) end state.user_selection:show( - state.selection.content .. '\n', + state.selection.content .. '\n\n', state.selection.filetype, state.chat.winnr ) From b34bf66b0e8c5c0b23dc8f95aa8abbd0515b0287 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 00:27:25 +0100 Subject: [PATCH 203/338] feat: add quickfix list for code blocks in chat Add new mapping 'gq' that populates quickfix list with code blocks from AI responses. Each entry includes source file and line numbers if available, making it easier to navigate through suggested code changes. Also fixes selection update to ignore special window types. --- README.md | 3 + lua/CopilotChat/config.lua | 4 ++ lua/CopilotChat/init.lua | 113 ++++++++++++++++++++++++++++++++----- 3 files changed, 105 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 94e69375..dbcbf41a 100644 --- a/README.md +++ b/README.md @@ -505,6 +505,9 @@ Also see [here](/lua/CopilotChat/config.lua): jump_to_diff = { normal = 'gj', }, + quickfix_diffs = { + normal = 'gq', + }, yank_diff = { normal = 'gy', register = '"', diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index becf0cbd..3f5556a8 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -61,6 +61,7 @@ local utils = require('CopilotChat.utils') ---@field toggle_sticky CopilotChat.config.mapping? ---@field accept_diff CopilotChat.config.mapping? ---@field jump_to_diff CopilotChat.config.mapping? +---@field quickfix_diffs CopilotChat.config.mapping? ---@field yank_diff CopilotChat.config.mapping? ---@field show_diff CopilotChat.config.mapping? ---@field show_system_prompt CopilotChat.config.mapping? @@ -333,6 +334,9 @@ return { jump_to_diff = { normal = 'gj', }, + quickfix_diffs = { + normal = 'gq', + }, yank_diff = { normal = 'gy', register = '"', diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index eb8baf9a..e593822d 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -112,7 +112,7 @@ end --- Updates the selection based on previous window local function update_selection() local prev_winnr = vim.fn.win_getid(vim.fn.winnr('#')) - if prev_winnr ~= state.chat.winnr then + if prev_winnr ~= state.chat.winnr and vim.fn.win_gettype(prev_winnr) == '' then state.source = { bufnr = vim.api.nvim_win_get_buf(prev_winnr), winnr = prev_winnr, @@ -137,6 +137,28 @@ local function update_selection() highlight_selection() end +---@return string?, number?, number? +local function match_header(header) + if not header then + return + end + + local header_filename, header_start_line, header_end_line = + header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') + if not header_filename then + header_filename, header_start_line, header_end_line = + header:match('%[file:(.+)%] line:(%d+)-(%d+)') + end + + if header_filename then + header_filename = vim.fn.fnamemodify(header_filename, ':p:.') + header_start_line = tonumber(header_start_line) or 1 + header_end_line = tonumber(header_end_line) or header_start_line + end + + return header_filename, header_start_line, header_end_line +end + ---@return CopilotChat.Diff.diff|nil local function get_diff() local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) @@ -153,20 +175,7 @@ local function get_diff() -- Try to get header info first local header = section[change_start - 2] - local header_filename, header_start_line, header_end_line - if header then - header_filename, header_start_line, header_end_line = - header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') - if not header_filename then - header_filename, header_start_line, header_end_line = - header:match('%[file:(.+)%] line:(%d+)-(%d+)') - end - if header_filename then - header_filename = vim.fn.fnamemodify(header_filename, ':p:.') - header_start_line = tonumber(header_start_line) or 1 - header_end_line = tonumber(header_end_line) or header_start_line - end - end + local header_filename, header_start_line, header_end_line = match_header(header) -- Initialize variables with selection if available local reference = state.selection and state.selection.content @@ -1097,6 +1106,80 @@ function M.setup(config) update_selection() end) + map_key(M.config.mappings.quickfix_diffs, bufnr, function() + local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local items = {} + local in_block = false + local block_start = 0 + local filetype = '' + local in_answer = false + local last_header = nil + + for i, line in ipairs(chat_lines) do + -- Track if we're in an AI response section + if line:match(M.config.answer_header .. M.config.separator .. '$') then + in_answer = true + elseif line:match(M.config.question_header .. M.config.separator .. '$') then + in_answer = false + end + + -- Only process code blocks in AI responses + if in_answer then + -- Try to capture markdown header with file info + local filename, start_line, end_line = match_header(line) + if filename then + last_header = { + filename = filename, + start_line = start_line, + end_line = end_line, + } + end + + if line:match('^```%w+$') then + in_block = true + block_start = i + 1 + filetype = line:match('^```(%w+)$') + elseif line == '```' and in_block then + in_block = false + local item = { + bufnr = bufnr, + lnum = block_start, + end_lnum = i - 1, + } + + if last_header then + item.text = string.format( + '%s [lines %d-%d]', + last_header.filename, + last_header.start_line, + last_header.end_line + ) + elseif + state.selection + and state.selection.filename + and state.selection.start_line + and state.selection.end_line + then + item.text = string.format( + '%s [lines %d-%d]', + state.selection.filename, + state.selection.start_line, + state.selection.end_line + ) + else + item.text = string.format('Code block (%s)', filetype) + end + + table.insert(items, item) + last_header = nil + end + end + end + + vim.fn.setqflist(items) + vim.cmd('copen') + end) + map_key(M.config.mappings.yank_diff, bufnr, function() local diff = get_diff() if not diff then From 75e9f938b15aff0590280711735c2b7677fe5010 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Nov 2024 23:44:45 +0000 Subject: [PATCH 204/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9fe6b8d2..f151a694 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -555,6 +555,9 @@ Also see here : jump_to_diff = { normal = 'gj', }, + quickfix_diffs = { + normal = 'gq', + }, yank_diff = { normal = 'gy', register = '"', From 5666afd9edab9b3b7f3d2f2866ce9aac2bee4e3e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 01:28:42 +0100 Subject: [PATCH 205/338] refactor: cleanup selection state management Refactor selection state management to use getter function instead of storing selection in global state. This improves code maintainability by: - Removing mutable selection state from global state object - Adding type-safe get_selection() function to encapsulate selection logic - Ensuring selection is always retrieved from current source - Moving selection logic closer to where it's used The change also fixes a bug where header info was not properly validated before being used as source of truth in get_diff(). --- lua/CopilotChat/init.lua | 143 +++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 75 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index e593822d..2ababf4c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -16,9 +16,7 @@ local plugin_name = 'CopilotChat.nvim' --- @class CopilotChat.state --- @field copilot CopilotChat.Copilot? --- @field source CopilotChat.config.source? ---- @field selection CopilotChat.config.selection? --- @field config CopilotChat.config? ---- @field last_system_prompt string? --- @field last_prompt string? --- @field last_response string? --- @field chat CopilotChat.Chat? @@ -31,11 +29,9 @@ local state = { -- Current state tracking source = nil, - selection = nil, config = nil, -- Last state tracking - last_system_prompt = nil, last_prompt = nil, last_response = nil, @@ -47,6 +43,27 @@ local state = { help = nil, } +---@return CopilotChat.config.selection? +local function get_selection() + local bufnr = state.source and state.source.bufnr + local winnr = state.source and state.source.winnr + + if + state.config + and state.config.selection + and utils.buf_valid(bufnr) + and winnr + and vim.api.nvim_win_is_valid(winnr) + then + return state.config.selection(state.source) + end + + return nil +end + +---@param prompt string +---@param system_prompt string +---@return string, string local function update_prompts(prompt, system_prompt) local prompts_to_use = M.prompts() local try_again = false @@ -88,25 +105,16 @@ local function highlight_selection(clear) return end - if - not state.selection - or not utils.buf_valid(state.selection.bufnr) - or not state.selection.start_line - then + local selection = get_selection() + if not selection or not selection.start_line or not utils.buf_valid(selection.bufnr) then return end - vim.api.nvim_buf_set_extmark( - state.selection.bufnr, - selection_ns, - state.selection.start_line - 1, - 0, - { - hl_group = 'CopilotChatSelection', - end_row = state.selection.end_line, - strict = false, - } - ) + vim.api.nvim_buf_set_extmark(selection.bufnr, selection_ns, selection.start_line - 1, 0, { + hl_group = 'CopilotChatSelection', + end_row = selection.end_line, + strict = false, + }) end --- Updates the selection based on previous window @@ -119,21 +127,6 @@ local function update_selection() } end - local bufnr = state.source and state.source.bufnr - local winnr = state.source and state.source.winnr - - if - state.config - and state.config.selection - and utils.buf_valid(bufnr) - and winnr - and vim.api.nvim_win_is_valid(winnr) - then - state.selection = state.config.selection(state.source) - else - state.selection = nil - end - highlight_selection() end @@ -178,15 +171,16 @@ local function get_diff() local header_filename, header_start_line, header_end_line = match_header(header) -- Initialize variables with selection if available - local reference = state.selection and state.selection.content - local start_line = state.selection and state.selection.start_line - local end_line = state.selection and state.selection.end_line - local filename = state.selection and state.selection.filename - local filetype = state.selection and state.selection.filetype - local bufnr = state.selection and state.selection.bufnr + local selection = get_selection() + local reference = selection and selection.content + local start_line = selection and selection.start_line + local end_line = selection and selection.end_line + local filename = selection and selection.filename + local filetype = selection and selection.filetype + local bufnr = selection and selection.bufnr -- If we have header info, use it as source of truth - if header_filename then + if header_filename and header_start_line and header_end_line then -- Try to find matching buffer and window bufnr = nil for _, win in ipairs(vim.api.nvim_list_wins()) do @@ -623,7 +617,6 @@ function M.ask(prompt, config) -- Resolve prompt references local system_prompt, updated_prompt = update_prompts(prompt or '', config.system_prompt) - state.last_system_prompt = system_prompt state.last_prompt = prompt -- Remove sticky prefix @@ -671,6 +664,9 @@ function M.ask(prompt, config) end local embeddings = vim.tbl_values(embedding_map) + -- Retrieve the selection + local selection = get_selection() + async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) local selected_agent = config.agent @@ -703,7 +699,7 @@ function M.ask(prompt, config) local ask_ok, response, token_count, token_max_count = pcall(state.copilot.ask, state.copilot, prompt, { - selection = state.selection, + selection = selection, embeddings = filtered_embeddings, system_prompt = system_prompt, model = selected_model, @@ -760,7 +756,6 @@ function M.stop(reset, config) wrap(function() if reset then state.chat:clear() - state.last_system_prompt = nil state.last_prompt = nil state.last_response = nil end @@ -992,22 +987,6 @@ function M.setup(config) map_key(M.config.mappings.close, bufnr, M.close) map_key(M.config.mappings.complete, bufnr, trigger_complete) - if M.config.chat_autocomplete then - vim.api.nvim_create_autocmd('TextChangedI', { - buffer = bufnr, - callback = function() - local line = vim.api.nvim_get_current_line() - local cursor = vim.api.nvim_win_get_cursor(0) - local col = cursor[2] - local char = line:sub(col, col) - - if vim.tbl_contains(M.complete_info().triggers, char) then - utils.debounce(trigger_complete, 100) - end - end, - }) - end - map_key(M.config.mappings.submit_prompt, bufnr, function() local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local current_line = vim.api.nvim_win_get_cursor(0)[1] @@ -1108,6 +1087,7 @@ function M.setup(config) map_key(M.config.mappings.quickfix_diffs, bufnr, function() local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local selection = get_selection() local items = {} local in_block = false local block_start = 0 @@ -1155,16 +1135,16 @@ function M.setup(config) last_header.end_line ) elseif - state.selection - and state.selection.filename - and state.selection.start_line - and state.selection.end_line + selection + and selection.filename + and selection.start_line + and selection.end_line then item.text = string.format( '%s [lines %d-%d]', - state.selection.filename, - state.selection.start_line, - state.selection.end_line + selection.filename, + selection.start_line, + selection.end_line ) else item.text = string.format('Code block (%s)', filetype) @@ -1199,7 +1179,7 @@ function M.setup(config) end) map_key(M.config.mappings.show_system_prompt, bufnr, function() - local prompt = state.last_system_prompt or M.config.system_prompt + local prompt = state.config.system_prompt if not prompt then return end @@ -1208,15 +1188,12 @@ function M.setup(config) end) map_key(M.config.mappings.show_user_selection, bufnr, function() - if not state.selection or not state.selection.content then + local selection = get_selection() + if not selection or not selection.content then return end - state.user_selection:show( - state.selection.content .. '\n\n', - state.selection.filetype, - state.chat.winnr - ) + state.user_selection:show(selection.content .. '\n\n', selection.filetype, state.chat.winnr) end) vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { @@ -1243,6 +1220,22 @@ function M.setup(config) }) end + if M.config.chat_autocomplete then + vim.api.nvim_create_autocmd('TextChangedI', { + buffer = bufnr, + callback = function() + local line = vim.api.nvim_get_current_line() + local cursor = vim.api.nvim_win_get_cursor(0) + local col = cursor[2] + local char = line:sub(col, col) + + if vim.tbl_contains(M.complete_info().triggers, char) then + utils.debounce(trigger_complete, 100) + end + end, + }) + end + finish(M.config, nil, nil, true) end ) From db939ceaf45f68677e20fa1c2f7ff021525b3467 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 20 Nov 2024 00:31:38 +0000 Subject: [PATCH 206/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index f151a694..051ae80e 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 19 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 20 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From d24c4b5b4ae1c27041dc931fbcc732a6829c7d9e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 01:51:17 +0100 Subject: [PATCH 207/338] fix: move line endings to overlay.show function Previously, line endings were inconsistently added in different show calls. This change centralizes the line ending management in the overlay.show function, making it more maintainable and consistent across all usages. The changes include: - Remove duplicate line endings from individual show calls - Add line ending handling in overlay.show function - Clean up trailing whitespace in system prompt display --- lua/CopilotChat/init.lua | 5 ++--- lua/CopilotChat/overlay.lua | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 2ababf4c..8f4def10 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -979,7 +979,6 @@ function M.setup(config) end end end - chat_help = chat_help .. M.config.separator .. '\n' state.help:show(chat_help, 'markdown', state.chat.winnr) end) @@ -1184,7 +1183,7 @@ function M.setup(config) return end - state.system_prompt:show(prompt .. '\n\n', 'markdown', state.chat.winnr) + state.system_prompt:show(vim.trim(prompt) .. '\n', 'markdown', state.chat.winnr) end) map_key(M.config.mappings.show_user_selection, bufnr, function() @@ -1193,7 +1192,7 @@ function M.setup(config) return end - state.user_selection:show(selection.content .. '\n\n', selection.filetype, state.chat.winnr) + state.user_selection:show(selection.content .. '\n', selection.filetype, state.chat.winnr) end) vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index fae41044..d0f25183 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -38,6 +38,8 @@ end function Overlay:show(text, filetype, winnr, syntax) self:validate() + text = text .. '\n' + vim.api.nvim_win_set_buf(winnr, self.bufnr) vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, vim.split(text, '\n')) From 08bcca1501ab8cd9b872f4906f9187ea1e9e5143 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 09:26:29 +0100 Subject: [PATCH 208/338] docs: Add documentation about chat mappings and README image (#553) * docs: Add documentation about chat mappings and README image * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index dbcbf41a..be293fb2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ [![Dotfyle](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat)](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim) [![All Contributors](https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat&link=%23contributors-)](#contributors) +![image](https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea) + > [!NOTE] > Plugin was rewritten to Lua from Python. Please check the [migration guide from version 1 to version 2](/MIGRATION.md) for more information. @@ -17,6 +19,7 @@ - [Post-Installation](#post-installation) - [Usage](#usage) - [Commands](#commands) + - [Chat Mappings](#chat-mappings) - [Prompts](#prompts) - [System Prompts](#system-prompts) - [Sticky Prompts](#sticky-prompts) @@ -130,6 +133,41 @@ See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/ma - `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. - `:CopilotChatAgents` - View and select available agents. This is reset when a new instance is made. Please set your agent in `init.lua` for persistence. +### Chat Mappings + +- `` - Complete the current input +- `q`/`` - Close the chat window +- `` - Reset/clear the chat window +- ``/`` - Submit the current prompt +- `gr` - Toggle sticky prompt - makes line under cursor sticky or deletes sticky line +- `` - Accept current diff changes +- `gj` - Jump to buffer that suggested change is modifying (if buffer is not available, it will be created) +- `gq` - Add all diffs to quickfix list +- `gy` - Yank current diff to register (default register is `"`) +- `gd` - Show diff between source and suggested change +- `gp` - Show system prompt used for the current chat +- `gs` - Show current user selection +- `gh` - Show help message + +The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: + +- `normal`: Key for normal mode +- `insert`: Key for insert mode +- `detail`: Description of what the mapping does + +For example, to change the submit prompt mapping: + +```lua +{ + mappings = { + submit_prompt = { + normal = 's', + insert = '' + } + } +} +``` + ### Prompts You can ask Copilot to do various tasks with prompts. You can reference prompts with `/PromptName` in chat or call with command `:CopilotChat`. From e07cae20322aff08fc5e21138cc88bd5d6518399 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 20 Nov 2024 08:26:54 +0000 Subject: [PATCH 209/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 55 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 051ae80e..1213da59 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -33,6 +33,7 @@ Table of Contents *CopilotChat-table-of-contents* - |CopilotChat-post-installation| - |CopilotChat-usage| - |CopilotChat-commands| + - |CopilotChat-chat-mappings| - |CopilotChat-prompts| - |CopilotChat-system-prompts| - |CopilotChat-sticky-prompts| @@ -160,6 +161,43 @@ COMMANDS ~ - `:CopilotChatAgents` - View and select available agents. This is reset when a new instance is made. Please set your agent in `init.lua` for persistence. +CHAT MAPPINGS ~ + +- `` - Complete the current input +- `q`/`` - Close the chat window +- `` - Reset/clear the chat window +- ``/`` - Submit the current prompt +- `gr` - Toggle sticky prompt - makes line under cursor sticky or deletes sticky line +- `` - Accept current diff changes +- `gj` - Jump to buffer that suggested change is modifying (if buffer is not available, it will be created) +- `gq` - Add all diffs to quickfix list +- `gy` - Yank current diff to register (default register is `"`) +- `gd` - Show diff between source and suggested change +- `gp` - Show system prompt used for the current chat +- `gs` - Show current user selection +- `gh` - Show help message + +The mappings can be customized by setting the `mappings` table in your +configuration. Each mapping can have: + +- `normal`: Key for normal mode +- `insert`: Key for insert mode +- `detail`: Description of what the mapping does + +For example, to change the submit prompt mapping: + +>lua + { + mappings = { + submit_prompt = { + normal = 's', + insert = '' + } + } + } +< + + PROMPTS ~ You can ask Copilot to do various tasks with prompts. You can reference prompts @@ -747,14 +785,15 @@ STARGAZERS OVER TIME ~ 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg 4. *Dotfyle*: https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat 5. *All Contributors*: https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat&link=%23contributors- -6. *@jellydn*: -7. *@deathbeam*: -8. *Chat with buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif -9. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c -10. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b -11. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 -12. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 -13. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg +6. *image*: https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea +7. *@jellydn*: +8. *@deathbeam*: +9. *Chat with buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif +10. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c +11. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b +12. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 +13. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 +14. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From 3077c4953208f88a60e1c7e46ec76aa913cd4ecd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 09:29:16 +0100 Subject: [PATCH 210/338] fix: allow single-line visual selections Previously, the visual selection function would return nil when start and end lines were the same, preventing single-line selections from working. This change removes this limitation, making the behavior more intuitive. Closes #552 Signed-off-by: Tomas Slusny --- lua/CopilotChat/select.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 4b536501..89fd8e08 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -41,11 +41,6 @@ function M.visual(source) local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '<')) local finish_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '>')) - -- Exit if no actual selection - if start_line == finish_line then - return nil - end - -- Swap positions if selection is backwards if start_line > finish_line then start_line, finish_line = finish_line, start_line From e6ddcee2eb2f250d4b2a8ca571a6550cf1859597 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 10:20:38 +0100 Subject: [PATCH 211/338] Mention that visual selections include diagnostics as well in README Signed-off-by: Tomas Slusny --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index be293fb2..ab32435d 100644 --- a/README.md +++ b/README.md @@ -313,9 +313,9 @@ Selections are configurable either by default or by prompt. Default selection is `visual` or `buffer` (if no visual selection). Default supported selections that live in `local select = require("CopilotChat.select")` are: -- `select.visual` - Current visual selection. Works well with diffs. -- `select.buffer` - Current buffer content. Works well with diffs. -- `select.line` - Current line content. Works decently with diffs. +- `select.visual` - Current visual selection. Includes diagnostic in selection. Works well with diffs. +- `select.buffer` - Current buffer content. Includes diagnostic in selection. Works well with diffs. +- `select.line` - Current line content. Includes diagnostic in selection. Works decently with diffs. - `select.unnamed` - Content from the unnamed register. - `select.clipboard` - Content from system clipboard. From 60cc34d636574e73f844003759c4d61271aa92b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 20 Nov 2024 09:26:25 +0000 Subject: [PATCH 212/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 1213da59..14c32dec 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -360,9 +360,9 @@ Default selection is `visual` or `buffer` (if no visual selection). Default supported selections that live in `local select = require("CopilotChat.select")` are: -- `select.visual` - Current visual selection. Works well with diffs. -- `select.buffer` - Current buffer content. Works well with diffs. -- `select.line` - Current line content. Works decently with diffs. +- `select.visual` - Current visual selection. Includes diagnostic in selection. Works well with diffs. +- `select.buffer` - Current buffer content. Includes diagnostic in selection. Works well with diffs. +- `select.line` - Current line content. Includes diagnostic in selection. Works decently with diffs. - `select.unnamed` - Content from the unnamed register. - `select.clipboard` - Content from system clipboard. From 734d38c3f7605754e369d3d2b5a06b2b77ad9a03 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 11:30:58 +0100 Subject: [PATCH 213/338] feat(chat): add no_chat option Add new configuration option for suppressing output and chat history storage. This enables using CopilotChat purely as a backend service by handling responses through callbacks without any UI interaction or state persistence. See #551 Signed-off-by: Tomas Slusny --- README.md | 1 + lua/CopilotChat/config.lua | 2 + lua/CopilotChat/copilot.lua | 20 ++++++--- lua/CopilotChat/init.lua | 81 +++++++++++++++++++++++-------------- 4 files changed, 68 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index ab32435d..ce109fdb 100644 --- a/README.md +++ b/README.md @@ -449,6 +449,7 @@ Also see [here](/lua/CopilotChat/config.lua): history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received + no_chat = false, -- Do not write to chat buffer and use chat history (useful for using callback for custom processing) -- default selection selection = function(source) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 3f5556a8..581258fb 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -93,6 +93,7 @@ local utils = require('CopilotChat.utils') ---@field highlight_headers boolean? ---@field history_path string? ---@field callback fun(response: string, source: CopilotChat.config.source)? +---@field no_chat boolean? ---@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.config.selection? ---@field contexts table? ---@field prompts table? @@ -127,6 +128,7 @@ return { history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received + no_chat = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) -- default selection selection = function(source) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 481838b6..db4ba2cb 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -10,6 +10,7 @@ ---@field model string? ---@field agent string? ---@field temperature number? +---@field no_history boolean? ---@field on_progress nil|fun(response: string):nil ---@class CopilotChat.copilot.embed.opts @@ -566,6 +567,7 @@ function Copilot:ask(prompt, opts) local model = opts.model or 'gpt-4o-2024-05-13' local agent = opts.agent or 'copilot' local temperature = opts.temperature or 0.1 + local no_history = opts.no_history or false local on_progress = opts.on_progress local job_id = uuid() self.current_job = job_id @@ -578,6 +580,7 @@ function Copilot:ask(prompt, opts) log.debug('Agent: ' .. agent) log.debug('Temperature: ' .. temperature) + local history = no_history and {} or self.history local models = self:fetch_models() local agents = self:fetch_agents() local agent_config = agents[agent] @@ -618,11 +621,11 @@ function Copilot:ask(prompt, opts) -- Calculate how many tokens we can use for history local history_limit = max_tokens - required_tokens - reserved_tokens - local history_tokens = count_history_tokens(self.history) + local history_tokens = count_history_tokens(history) -- If we're over history limit, truncate history from the beginning - while history_tokens > history_limit and #self.history > 0 do - local removed = table.remove(self.history, 1) + while history_tokens > history_limit and #history > 0 do + local removed = table.remove(history, 1) history_tokens = history_tokens - tiktoken.count(removed.content) end @@ -740,7 +743,7 @@ function Copilot:ask(prompt, opts) local is_stream = not vim.startswith(model, 'o1') local body = vim.json.encode( generate_ask_request( - self.history, + history, prompt, system_prompt, generated_messages, @@ -836,16 +839,21 @@ function Copilot:ask(prompt, opts) log.trace('Full response: ' .. full_response) log.debug('Last message: ' .. vim.inspect(last_message)) - table.insert(self.history, { + table.insert(history, { content = prompt, role = 'user', }) - table.insert(self.history, { + table.insert(history, { content = full_response, role = 'assistant', }) + if not no_history then + log.debug('History size increased to ' .. #history) + self.history = history + end + return full_response, last_message and last_message.usage and last_message.usage.total_tokens, max_tokens diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 8f4def10..f0b6b750 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -43,14 +43,15 @@ local state = { help = nil, } +---@param config CopilotChat.config ---@return CopilotChat.config.selection? -local function get_selection() +local function get_selection(config) local bufnr = state.source and state.source.bufnr local winnr = state.source and state.source.winnr if - state.config - and state.config.selection + config + and config.selection and utils.buf_valid(bufnr) and winnr and vim.api.nvim_win_is_valid(winnr) @@ -105,7 +106,7 @@ local function highlight_selection(clear) return end - local selection = get_selection() + local selection = get_selection(state.config) if not selection or not selection.start_line or not utils.buf_valid(selection.bufnr) then return end @@ -171,7 +172,7 @@ local function get_diff() local header_filename, header_start_line, header_end_line = match_header(header) -- Initialize variables with selection if available - local selection = get_selection() + local selection = get_selection(state.config) local reference = selection and selection.content local start_line = selection and selection.start_line local end_line = selection and selection.end_line @@ -244,6 +245,10 @@ local function apply_diff(diff) end local function finish(config, message, hide_help, start_of_chat) + if config.no_chat then + return + end + if not start_of_chat then state.chat:append('\n\n') end @@ -274,9 +279,13 @@ local function finish(config, message, hide_help, start_of_chat) end end -local function show_error(err, config, append_newline) +local function show_error(config, err, append_newline) log.error(vim.inspect(err)) + if config.no_chat then + return + end + if type(err) == 'string' then local message = err:match('^[^:]+:[^:]+:(.+)') or err message = message:gsub('^%s*', '') @@ -591,33 +600,39 @@ end function M.ask(prompt, config) config = vim.tbl_deep_extend('force', M.config, config or {}) vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot_diagnostics')) - M.open(config) + + if not config.no_chat then + M.open(config) + end prompt = vim.trim(prompt or '') if prompt == '' then return end - if config.clear_chat_on_new_prompt then - M.stop(true, config) - elseif state.copilot:stop() then - finish(config, nil, true) - end + if not config.no_chat then + if config.clear_chat_on_new_prompt then + M.stop(true, config) + elseif state.copilot:stop() then + finish(config, nil, true) + end - -- Clear the current input prompt before asking a new question - local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) - local _, start_line, end_line = - utils.find_lines(chat_lines, #chat_lines, M.config.separator .. '$', nil, true) - if #chat_lines == end_line then - vim.api.nvim_buf_set_lines(state.chat.bufnr, start_line, end_line, false, { '' }) - end + state.last_prompt = prompt - state.chat:append(prompt) - state.chat:append('\n\n' .. config.answer_header .. config.separator .. '\n\n') + -- Clear the current input prompt before asking a new question + local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) + local _, start_line, end_line = + utils.find_lines(chat_lines, #chat_lines, M.config.separator .. '$', nil, true) + if #chat_lines == end_line then + vim.api.nvim_buf_set_lines(state.chat.bufnr, start_line, end_line, false, { '' }) + end + + state.chat:append(prompt) + state.chat:append('\n\n' .. config.answer_header .. config.separator .. '\n\n') + end -- Resolve prompt references - local system_prompt, updated_prompt = update_prompts(prompt or '', config.system_prompt) - state.last_prompt = prompt + local system_prompt, updated_prompt = update_prompts(prompt, config.system_prompt) -- Remove sticky prefix prompt = table.concat( @@ -665,7 +680,7 @@ function M.ask(prompt, config) local embeddings = vim.tbl_values(embedding_map) -- Retrieve the selection - local selection = get_selection() + local selection = get_selection(config) async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) @@ -692,7 +707,7 @@ function M.ask(prompt, config) if not query_ok then vim.schedule(function() - show_error(filtered_embeddings, config, has_output) + show_error(config, filtered_embeddings, has_output) end) return end @@ -705,9 +720,13 @@ function M.ask(prompt, config) model = selected_model, agent = selected_agent, temperature = config.temperature, + no_history = config.no_chat, on_progress = function(token) vim.schedule(function() - state.chat:append(token) + if not config.no_chat then + state.chat:append(token) + end + has_output = true end) end, @@ -715,7 +734,7 @@ function M.ask(prompt, config) if not ask_ok then vim.schedule(function() - show_error(response, config, has_output) + show_error(config, response, has_output) end) return end @@ -724,7 +743,9 @@ function M.ask(prompt, config) return end - state.last_response = response + if not config.no_chat then + state.last_response = response + end vim.schedule(function() if token_count and token_max_count and token_count > 0 then @@ -1086,7 +1107,7 @@ function M.setup(config) map_key(M.config.mappings.quickfix_diffs, bufnr, function() local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local selection = get_selection() + local selection = get_selection(state.config) local items = {} local in_block = false local block_start = 0 @@ -1187,7 +1208,7 @@ function M.setup(config) end) map_key(M.config.mappings.show_user_selection, bufnr, function() - local selection = get_selection() + local selection = get_selection(state.config) if not selection or not selection.content then return end From 8b257dd79adc3ff86192ae121c10802c2ab08540 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 20 Nov 2024 11:08:14 +0000 Subject: [PATCH 214/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 14c32dec..61c870e9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -499,6 +499,7 @@ Also see here : history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history callback = nil, -- Callback to use when ask response is received + no_chat = false, -- Do not write to chat buffer and use chat history (useful for using callback for custom processing) -- default selection selection = function(source) From ce987073fb65e762048c5c0c0f11bde5a0a94d81 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 12:32:16 +0100 Subject: [PATCH 215/338] Remove forgotten log entry Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index db4ba2cb..c8dc53b8 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -298,7 +298,6 @@ local function generate_ask_request( end if not vim.tbl_isempty(contexts) then - log.info('Contexts found') prompt = table.concat(vim.tbl_keys(contexts), '\n') .. '\n' .. prompt end From 4bb57a4b26b9d08770403f6613a227e852d51393 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 12:44:21 +0100 Subject: [PATCH 216/338] refactor(prompts): improve diagnostics handling system Streamline the way diagnostics are handled in prompts by: - Move diagnostics check from general prompt into explain prompt - Split explain prompt into multiple lines for better readability - Update documentation and config to reflect these changes This creates a more focused approach where diagnostics are examined as part of code explanation rather than being a separate concern. --- README.md | 4 ++-- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/prompts.lua | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ce109fdb..48a7a0e9 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ For example, to change the submit prompt mapping: You can ask Copilot to do various tasks with prompts. You can reference prompts with `/PromptName` in chat or call with command `:CopilotChat`. Default prompts are: -- `Explain` - Write an explanation for the selected code and diagnostics as paragraphs of text +- `Explain` - Write an explanation for the selected code as paragraphs of text - `Review` - Review the selected code - `Fix` - There is a problem in this code. Rewrite the code to show it with the bug fixed - `Optimize` - Optimize the selected code to improve performance and readability @@ -478,7 +478,7 @@ Also see [here](/lua/CopilotChat/config.lua): -- default prompts prompts = { Explain = { - prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', + prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code as paragraphs of text.', }, Review = { prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 581258fb..835c9705 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -232,7 +232,7 @@ return { -- default prompts prompts = { Explain = { - prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', + prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code as paragraphs of text.', }, Review = { prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', diff --git a/lua/CopilotChat/prompts.lua b/lua/CopilotChat/prompts.lua index 088371e0..7a7a0cb8 100644 --- a/lua/CopilotChat/prompts.lua +++ b/lua/CopilotChat/prompts.lua @@ -21,7 +21,10 @@ You are an AI programming assistant. ]] .. base M.COPILOT_EXPLAIN = [[ -You are a world-class coding tutor. Your code explanations perfectly balance high-level concepts and granular details. Your approach ensures that students not only understand how to write code, but also grasp the underlying principles that guide effective programming. +You are a world-class coding tutor. +Your code explanations perfectly balance high-level concepts and granular details. +Your approach ensures that students not only understand how to write code, but also grasp the underlying principles that guide effective programming. +When examining code pay close attention to diagnostics as well. When explaining diagnostics, include diagnostic content in your response. ]] .. base M.COPILOT_REVIEW = M.COPILOT_INSTRUCTIONS @@ -82,7 +85,7 @@ Your task is to modify the provided code according to the user's request. Follow 9. Directly above every returned code snippet, add `[file:]() line:-`. Example: `[file:copilot.lua](nvim/.config/nvim/lua/config/copilot.lua) line:1-98`. This is markdown link syntax, so make sure to follow it. -10. When examining code pay close attention to diagnostics. When fixing diagnostics, add the diagnostic message as a comment next to the line where the fix was applied. +10. When fixing code pay close attention to diagnostics as well. When fixing diagnostics, include diagnostic content in your response. Remember that Your response SHOULD CONTAIN ONLY THE MODIFIED CODE to be used as DIRECT REPLACEMENT to the original file. ]] From d32648aa0d78fefd95a1dc4d64f85538e05556ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 20 Nov 2024 11:46:12 +0000 Subject: [PATCH 217/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 61c870e9..4d65e2ae 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -204,7 +204,7 @@ You can ask Copilot to do various tasks with prompts. You can reference prompts with `/PromptName` in chat or call with command `:CopilotChat`. Default prompts are: -- `Explain` - Write an explanation for the selected code and diagnostics as paragraphs of text +- `Explain` - Write an explanation for the selected code as paragraphs of text - `Review` - Review the selected code - `Fix` - There is a problem in this code. Rewrite the code to show it with the bug fixed - `Optimize` - Optimize the selected code to improve performance and readability @@ -528,7 +528,7 @@ Also see here : -- default prompts prompts = { Explain = { - prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code and diagnostics as paragraphs of text.', + prompt = '> /COPILOT_EXPLAIN\n\nWrite an explanation for the selected code as paragraphs of text.', }, Review = { prompt = '> /COPILOT_REVIEW\n\nReview the selected code.', From 5ff64d0900d019690e6fc16ee387fac02ccd4ea6 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 12:53:10 +0100 Subject: [PATCH 218/338] refactor: remove redundant treesitter parser init The treesitter parser initialization for markdown was redundant since the buffer's syntax is already set to markdown via vim.bo[bufnr].syntax. --- lua/CopilotChat/chat.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index d3e9bae3..d2314afc 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -52,10 +52,6 @@ local Chat = class(function(self, help, on_buf_create) vim.bo[bufnr].filetype = self.name vim.bo[bufnr].syntax = 'markdown' vim.bo[bufnr].textwidth = 0 - local ok, parser = pcall(vim.treesitter.get_parser, bufnr, 'markdown') - if ok and parser then - vim.treesitter.start(bufnr, 'markdown') - end if not self.spinner then self.spinner = Spinner(bufnr) From 0d76668e138d6e509538c491783595e8610be35a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 13:39:13 +0100 Subject: [PATCH 219/338] fix: respect completion menu in insert mode mappings When in insert mode and completion menu is visible, use the original key mapping instead of triggering the custom function. This prevents interference with completion menu navigation. Additionally, improve cursor positioning after inserting completion text by moving it to the end of inserted value. --- lua/CopilotChat/init.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f0b6b750..f1eb8e26 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -314,7 +314,15 @@ local function map_key(key, bufnr, fn) vim.keymap.set('n', key.normal, fn, { buffer = bufnr, nowait = true }) end if key.insert and key.insert ~= '' then - vim.keymap.set('i', key.insert, fn, { buffer = bufnr }) + vim.keymap.set('i', key.insert, function() + -- If in insert mode and menu visible, use original key + if vim.fn.pumvisible() == 1 then + local keys = vim.api.nvim_replace_termcodes(key.insert, true, false, true) + vim.api.nvim_feedkeys(keys, 'n', false) + else + fn() + end + end, { buffer = bufnr }) end end @@ -380,7 +388,9 @@ local function trigger_complete() return end - vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { tostring(value) }) + local value_str = tostring(value) + vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { value_str }) + vim.api.nvim_win_set_cursor(0, { row, col + #value_str }) end) end From 1062c99b272aeb2c0ecf0c963a8a4c6deb0abdda Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 13:48:39 +0100 Subject: [PATCH 220/338] Make insert complete mapping accept completion if pum is visible Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f1eb8e26..8427d293 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -317,8 +317,14 @@ local function map_key(key, bufnr, fn) vim.keymap.set('i', key.insert, function() -- If in insert mode and menu visible, use original key if vim.fn.pumvisible() == 1 then - local keys = vim.api.nvim_replace_termcodes(key.insert, true, false, true) - vim.api.nvim_feedkeys(keys, 'n', false) + local used_key = key.insert == M.config.mappings.complete.insert and '' or key.insert + if used_key then + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes(used_key, true, false, true), + 'n', + false + ) + end else fn() end From 5147d2ad40092cad04459dd0555efce8871c3dcc Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 14:24:09 +0100 Subject: [PATCH 221/338] docs: improve readme for mappings Signed-off-by: Tomas Slusny --- README.md | 24 +++++++++++++++--------- lua/CopilotChat/init.lua | 1 + 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 48a7a0e9..5614cf2b 100644 --- a/README.md +++ b/README.md @@ -135,17 +135,17 @@ See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/ma ### Chat Mappings -- `` - Complete the current input +- `` - Trigger completion menu for special tokens or accept current completion (see help) - `q`/`` - Close the chat window -- `` - Reset/clear the chat window +- `` - Reset and clear the chat window - ``/`` - Submit the current prompt -- `gr` - Toggle sticky prompt - makes line under cursor sticky or deletes sticky line -- `` - Accept current diff changes -- `gj` - Jump to buffer that suggested change is modifying (if buffer is not available, it will be created) -- `gq` - Add all diffs to quickfix list -- `gy` - Yank current diff to register (default register is `"`) -- `gd` - Show diff between source and suggested change -- `gp` - Show system prompt used for the current chat +- `gr` - Toggle sticky prompt for the line under cursor +- `` - Accept nearest diff (works best with `COPILOT_GENERATE` prompt) +- `gj` - Jump to section of nearest diff. If in different buffer, jumps there; creates buffer if needed (works best with `COPILOT_GENERATE` prompt) +- `gq` - Add all diffs from chat to quickfix list +- `gy` - Yank nearest diff to register (defaults to `"`) +- `gd` - Show diff between source and nearest diff +- `gp` - Show system prompt for current chat - `gs` - Show current user selection - `gh` - Show help message @@ -306,6 +306,12 @@ You can define custom contexts like this: } ``` +```markdown +> #birthday:user + +What is my birthday +``` + ### Selections Selections are used to determine the source of the chat (so basically what to chat about). diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 8427d293..a4747ec3 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -994,6 +994,7 @@ function M.setup(config) local chat_help = '**`Special tokens`**\n' chat_help = chat_help .. '`@` to select an agent\n' chat_help = chat_help .. '`#` to select a context\n' + chat_help = chat_help .. '`#:` to select input for context\n' chat_help = chat_help .. '`/` to select a prompt\n' chat_help = chat_help .. '`$` to select a model\n' chat_help = chat_help .. '`> ` to make a sticky prompt (copied to next prompt)\n' From 32c9efa650f2f5c33dd18baad67959d55ad6c679 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 20 Nov 2024 13:28:34 +0000 Subject: [PATCH 222/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 4d65e2ae..6216fb9b 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -163,17 +163,17 @@ COMMANDS ~ CHAT MAPPINGS ~ -- `` - Complete the current input +- `` - Trigger completion menu for special tokens or accept current completion (see help) - `q`/`` - Close the chat window -- `` - Reset/clear the chat window +- `` - Reset and clear the chat window - ``/`` - Submit the current prompt -- `gr` - Toggle sticky prompt - makes line under cursor sticky or deletes sticky line -- `` - Accept current diff changes -- `gj` - Jump to buffer that suggested change is modifying (if buffer is not available, it will be created) -- `gq` - Add all diffs to quickfix list -- `gy` - Yank current diff to register (default register is `"`) -- `gd` - Show diff between source and suggested change -- `gp` - Show system prompt used for the current chat +- `gr` - Toggle sticky prompt for the line under cursor +- `` - Accept nearest diff (works best with `COPILOT_GENERATE` prompt) +- `gj` - Jump to section of nearest diff. If in different buffer, jumps there; creates buffer if needed (works best with `COPILOT_GENERATE` prompt) +- `gq` - Add all diffs from chat to quickfix list +- `gy` - Yank nearest diff to register (defaults to `"`) +- `gd` - Show diff between source and nearest diff +- `gp` - Show system prompt for current chat - `gs` - Show current user selection - `gh` - Show help message @@ -351,6 +351,12 @@ You can define custom contexts like this: } < +>markdown + > #birthday:user + + What is my birthday +< + SELECTIONS ~ From f69ee54eb673d0fa56810544f2f497b5274837d1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 14:42:38 +0100 Subject: [PATCH 223/338] feat: add trace logging for models and agents Add detailed trace logging using vim.inspect for models and agents data structures to help with debugging and development. --- lua/CopilotChat/copilot.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index c8dc53b8..e4858fb2 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -479,6 +479,7 @@ function Copilot:fetch_models() end log.info('Models fetched') + log.trace(vim.inspect(models)) self.models = out return out end @@ -512,6 +513,7 @@ function Copilot:fetch_agents() out['copilot'] = { name = 'Copilot', default = true, description = 'Default noop agent' } log.info('Agents fetched') + log.trace(vim.inspect(agents)) self.agents = out return out end From dfb88466fd93ccd2556518eb2e085cf38d554697 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 15:24:43 +0100 Subject: [PATCH 224/338] refactor: move utility functions to utils module Moves uuid, machine_id and quick_hash functions from copilot.lua to utils.lua module. Also improves type annotations and documentation for the moved functions. Removes unused table_equals function and renames blend_color_with_neovim_bg to blend_color for better clarity. --- lua/CopilotChat/copilot.lua | 58 +++++++++++------------------------ lua/CopilotChat/diff.lua | 18 ++--------- lua/CopilotChat/utils.lua | 61 ++++++++++++++++++++++--------------- 3 files changed, 57 insertions(+), 80 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index e4858fb2..cf64849f 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -36,6 +36,8 @@ local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') local class = utils.class local temp_file = utils.temp_file + +--- Constants local context_format = '[#file:%s](#file:%s-context)\n' local big_file_threshold = 2000 local timeout = 30000 @@ -81,31 +83,8 @@ local tiktoken_load = async.wrap(function(tokenizer, callback) tiktoken.load(tokenizer, callback) end, 2) -local function uuid() - local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' - return ( - string.gsub(template, '[xy]', function(c) - local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) - return string.format('%x', v) - end) - ) -end - -local function machine_id() - local length = 65 - local hex_chars = '0123456789abcdef' - local hex = '' - for _ = 1, length do - local index = math.random(1, #hex_chars) - hex = hex .. hex_chars:sub(index, index) - end - return hex -end - -local function quick_hash(str) - return #str .. str:sub(1, 32) .. str:sub(-32) -end - +--- Get the github oauth cached token +---@return string|nil local function get_cached_token() -- loading token from the environment only in GitHub Codespaces local token = os.getenv('GITHUB_TOKEN') @@ -140,6 +119,10 @@ local function get_cached_token() return nil end +--- Generate line numbers for the given content +---@param content string: The content to generate line numbers for +---@param start_line number|nil: The starting line number +---@return string local function generate_line_numbers(content, start_line) local lines = vim.split(content, '\n') local truncated = false @@ -348,21 +331,13 @@ local function generate_embedding_request(inputs, model) } end -local function count_history_tokens(history) - local count = 0 - for _, msg in ipairs(history) do - count = count + tiktoken.count(msg.content) - end - return count -end - local Copilot = class(function(self, proxy, allow_insecure) self.history = {} self.embedding_cache = {} self.github_token = nil self.token = nil self.sessionid = nil - self.machineid = machine_id() + self.machineid = utils.machine_id() self.models = nil self.agents = nil self.claude_enabled = false @@ -404,7 +379,7 @@ function Copilot:authenticate() if not self.token or (self.token.expires_at and self.token.expires_at <= math.floor(os.time())) then - local sessionid = uuid() .. tostring(math.floor(os.time() * 1000)) + local sessionid = utils.uuid() .. tostring(math.floor(os.time() * 1000)) local headers = { ['authorization'] = 'token ' .. self.github_token, ['accept'] = 'application/json', @@ -434,7 +409,7 @@ function Copilot:authenticate() local headers = { ['authorization'] = 'Bearer ' .. self.token.token, - ['x-request-id'] = uuid(), + ['x-request-id'] = utils.uuid(), ['vscode-sessionid'] = self.sessionid, ['vscode-machineid'] = self.machineid, ['copilot-integration-id'] = 'vscode-chat', @@ -570,7 +545,7 @@ function Copilot:ask(prompt, opts) local temperature = opts.temperature or 0.1 local no_history = opts.no_history or false local on_progress = opts.on_progress - local job_id = uuid() + local job_id = utils.uuid() self.current_job = job_id log.trace('System prompt: ' .. system_prompt) @@ -622,7 +597,10 @@ function Copilot:ask(prompt, opts) -- Calculate how many tokens we can use for history local history_limit = max_tokens - required_tokens - reserved_tokens - local history_tokens = count_history_tokens(history) + local history_tokens = 0 + for _, msg in ipairs(history) do + history_tokens = history_tokens + tiktoken.count(msg.content) + end -- If we're over history limit, truncate history from the beginning while history_tokens > history_limit and #history > 0 do @@ -914,7 +892,7 @@ function Copilot:embed(inputs, opts) embed.filetype = embed.filetype or 'text' if embed.content then - local key = embed.filename .. quick_hash(embed.content) + local key = embed.filename .. utils.quick_hash(embed.content) if self.embedding_cache[key] then table.insert(cached_embeddings, self.embedding_cache[key]) else @@ -977,7 +955,7 @@ function Copilot:embed(inputs, opts) -- Cache embeddings for _, embedding in ipairs(out) do if embedding.content then - local key = embedding.filename .. quick_hash(embedding.content) + local key = embedding.filename .. utils.quick_hash(embedding.content) self.embedding_cache[key] = embedding end end diff --git a/lua/CopilotChat/diff.lua b/lua/CopilotChat/diff.lua index 6a39204e..f8ed47fe 100644 --- a/lua/CopilotChat/diff.lua +++ b/lua/CopilotChat/diff.lua @@ -20,21 +20,9 @@ local class = utils.class local Diff = class(function(self, help, on_buf_create) self.hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights') - vim.api.nvim_set_hl( - self.hl_ns, - '@diff.plus', - { bg = utils.blend_color_with_neovim_bg('DiffAdd', 20) } - ) - vim.api.nvim_set_hl( - self.hl_ns, - '@diff.minus', - { bg = utils.blend_color_with_neovim_bg('DiffDelete', 20) } - ) - vim.api.nvim_set_hl( - self.hl_ns, - '@diff.delta', - { bg = utils.blend_color_with_neovim_bg('DiffChange', 20) } - ) + vim.api.nvim_set_hl(self.hl_ns, '@diff.plus', { bg = utils.blend_color('DiffAdd', 20) }) + vim.api.nvim_set_hl(self.hl_ns, '@diff.minus', { bg = utils.blend_color('DiffDelete', 20) }) + vim.api.nvim_set_hl(self.hl_ns, '@diff.delta', { bg = utils.blend_color('DiffChange', 20) }) self.name = 'copilot-diff' self.help = help diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index e2379df0..b1e921de 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -69,32 +69,11 @@ function M.config_path() end end ---- Check if a table is equal to another table ----@param a table The first table ----@param b table The second table ----@return boolean -function M.table_equals(a, b) - if type(a) ~= type(b) then - return false - end - if type(a) ~= 'table' then - return a == b - end - for k, v in pairs(a) do - if not M.table_equals(v, b[k]) then - return false - end - end - for k, v in pairs(b) do - if not M.table_equals(v, a[k]) then - return false - end - end - return true -end - --- Blend a color with the neovim background -function M.blend_color_with_neovim_bg(color_name, blend) +---@param color_name string The color name +---@param blend number The blend percentage +---@return string? +function M.blend_color(color_name, blend) local color_int = vim.api.nvim_get_hl(0, { name = color_name }).fg local bg_int = vim.api.nvim_get_hl(0, { name = 'Normal' }).bg @@ -207,4 +186,36 @@ function M.filename_same(file1, file2) return vim.fn.fnamemodify(file1, ':p') == vim.fn.fnamemodify(file2, ':p') end +--- Generate a UUID +---@return string +function M.uuid() + local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + return ( + string.gsub(template, '[xy]', function(c) + local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) + return string.format('%x', v) + end) + ) +end + +--- Generate machine id +---@return string +function M.machine_id() + local length = 65 + local hex_chars = '0123456789abcdef' + local hex = '' + for _ = 1, length do + local index = math.random(1, #hex_chars) + hex = hex .. hex_chars:sub(index, index) + end + return hex +end + +--- Generate a quick hash +---@param str string The string to hash +---@return string +function M.quick_hash(str) + return #str .. str:sub(1, 32) .. str:sub(-32) +end + return M From 17da917aca96146189badf500a53d6d9aa853648 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 16:59:19 +0100 Subject: [PATCH 225/338] Make policy enabling generic Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 45 ++++++++++++++----------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index cf64849f..397e078b 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -334,6 +334,7 @@ end local Copilot = class(function(self, proxy, allow_insecure) self.history = {} self.embedding_cache = {} + self.policies = {} self.github_token = nil self.token = nil self.sessionid = nil @@ -448,6 +449,10 @@ function Copilot:fetch_models() local models = vim.json.decode(response.body)['data'] local out = {} for _, model in ipairs(models) do + if not model['policy'] or model['policy']['state'] == 'enabled' then + self.policies[model['id']] = true + end + if model['capabilities']['type'] == 'chat' then out[model['id']] = model end @@ -493,42 +498,29 @@ function Copilot:fetch_agents() return out end -function Copilot:enable_claude() - if self.claude_enabled then - return true +--- Enable policy for the given model if required +---@param model string: The model to enable policy for +function Copilot:enable_policy(model) + if self.policies[model] then + return end - local business_check = 'cannot enable policy inline for business users' - local business_msg = - 'Claude is probably enabled (for business users needs to be enabled manually).' - local response, err = curl_post( - 'https://api.githubcopilot.com/models/claude-3.5-sonnet/policy', + 'https://api.githubcopilot.com/models/' .. model .. '/policy', vim.tbl_extend('force', self.request_args, { headers = self:authenticate(), body = vim.json.encode({ state = 'enabled' }), }) ) - if err then - error(err) - end + self.policies[model] = true - -- Handle business user case - if response.status ~= 200 and string.find(tostring(response.body), business_check) then - self.claude_enabled = true - log.info(business_msg) - return true - end - - -- Handle errors - if response.status ~= 200 then - error('Failed to enable Claude: ' .. tostring(response.status)) + if err or response.status ~= 200 then + log.warn('Failed to enable policy for ' .. model .. ': ' .. vim.inspect(err or response.body)) + return end - self.claude_enabled = true - log.info('Claude enabled') - return true + log.info('Policy enabled for ' .. model) end --- Ask a question to Copilot @@ -733,10 +725,7 @@ function Copilot:ask(prompt, opts) ) ) - if vim.startswith(model, 'claude') then - self:enable_claude() - end - + self:enable_policy(model) local url = 'https://api.githubcopilot.com/chat/completions' if not agent_config.default then url = 'https://api.githubcopilot.com/agents/' .. agent .. '?chat' From 6b2b412da939e8bc1120d6a2e0323f1354f76aba Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 19:18:11 +0100 Subject: [PATCH 226/338] feat: add proc/template/macro declaration support Add additional declaration types to the outline_types table to improve code structure detection: - proc_declaration - template_declaration - macro_declaration This enhances the context gathering capability for more programming language constructs. Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 945e0b1c..81be2693 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -11,6 +11,9 @@ local outline_types = { 'function_declaration', 'method_definition', 'method_declaration', + 'proc_declaration', + 'template_declaration', + 'macro_declaration', 'constructor_declaration', 'class_definition', 'class_declaration', From 79896c4e6faa6521b454121a9c070cffb1e81df2 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 20 Nov 2024 23:30:19 +0100 Subject: [PATCH 227/338] feat: support for markdown outlines Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 81be2693..f751896f 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -22,6 +22,14 @@ local outline_types = { 'type_alias_declaration', 'import_statement', 'import_from_statement', + -- markdown + 'atx_h1_marker', + 'atx_h2_marker', + 'atx_h3_marker', + 'atx_h4_marker', + 'atx_h5_marker', + 'list_item', + 'block_quote', } local comment_types = { From b6150bc0420606645ba2f469e11ee4ecec34d10d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 00:47:43 +0100 Subject: [PATCH 228/338] fix: improve chat buffer rendering and state handling - Make chat buffer non-modifiable by default and only allow modifications during content updates and after response is finished - Join response progress undos together so user can undo whole ask/response at once - Rerender headers in chat window on text change automatically, which means that when users accidentally edits the separators in buffer the highlights do not break until next question is asked --- lua/CopilotChat/chat.lua | 19 ++++++++++++++++--- lua/CopilotChat/init.lua | 1 + lua/CopilotChat/overlay.lua | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index d2314afc..35720df9 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -52,6 +52,16 @@ local Chat = class(function(self, help, on_buf_create) vim.bo[bufnr].filetype = self.name vim.bo[bufnr].syntax = 'markdown' vim.bo[bufnr].textwidth = 0 + vim.bo[bufnr].modifiable = false + + vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, { + buffer = bufnr, + callback = function() + vim.defer_fn(function() + self:render() + end, 100) + end, + }) if not self.spinner then self.spinner = Spinner(bufnr) @@ -118,6 +128,7 @@ end function Chat:append(str) self:validate() + vim.bo[self.bufnr].modifiable = true if self:active() then utils.return_to_normal_mode() @@ -145,17 +156,19 @@ function Chat:append(str) last_column, vim.split(str, '\n') ) - self:render() if should_follow_cursor then self:follow() end + + vim.bo[self.bufnr].modifiable = false end function Chat:clear() self:validate() + vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) - self:render() + vim.bo[self.bufnr].modifiable = false end function Chat:open(config) @@ -233,7 +246,6 @@ function Chat:open(config) else vim.wo[self.winnr].foldcolumn = '0' end - self:render() end function Chat:close(bufnr) @@ -295,6 +307,7 @@ function Chat:finish(msg, offset) msg = self.help end + vim.bo[self.bufnr].modifiable = true self:show_help(msg, -offset) if self.auto_insert and self:active() then vim.cmd('startinsert') diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index a4747ec3..f1baed20 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -740,6 +740,7 @@ function M.ask(prompt, config) on_progress = function(token) vim.schedule(function() if not config.no_chat then + vim.cmd('undojoin') state.chat:append(token) end diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index d0f25183..e8411351 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -18,6 +18,7 @@ local Overlay = class(function(self, name, help, on_buf_create) self.buf_create = function() local bufnr = vim.api.nvim_create_buf(false, true) vim.bo[bufnr].filetype = name + vim.bo[bufnr].modifiable = false vim.api.nvim_buf_set_name(bufnr, name) return bufnr end From c9791a799875a2b1d83938dc8c18575667a51a9a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 01:17:29 +0100 Subject: [PATCH 229/338] fix: rerender chat after opening as well Prevents issue when rerendering is not triggered due to buffer being hidden but still being updated Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 35720df9..a4ed2fe8 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -246,6 +246,8 @@ function Chat:open(config) else vim.wo[self.winnr].foldcolumn = '0' end + + self:render() end function Chat:close(bufnr) From 7ea757d8bb357602cc41f926cf07bf726420040f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 Nov 2024 00:18:32 +0000 Subject: [PATCH 230/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 6216fb9b..fd6c6b8a 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 20 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 21 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 35b942fae3999167bf3477b39fff0d3c040bd802 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 09:08:34 +0100 Subject: [PATCH 231/338] refactor: improve chat output parsing and selection handling Reimplements chat section parsing to better handle code blocks, headers and sections by storing them in a structured way within the Chat class. This enables more reliable navigation between sections, code blocks and diffs. Key changes: - Add sections and blocks tracking in Chat class - Improve code block and header parsing reliability - Consolidate duplicate header parsing code - Add get_closest_section and get_closest_block helper methods - Refactor diff handling to use new block parsing - Clean up quickfix list generation using new block data structure Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 188 +++++++++++++++++++++-- lua/CopilotChat/init.lua | 309 +++++++++++++++----------------------- lua/CopilotChat/utils.lua | 66 +------- 3 files changed, 307 insertions(+), 256 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index a4ed2fe8..bd4aeb44 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -1,6 +1,9 @@ ---@class CopilotChat.Chat ---@field bufnr number ---@field winnr number +---@field sections table +---@field get_closest_section fun(self: CopilotChat.Chat): table|nil +---@field get_closest_block fun(self: CopilotChat.Chat): table|nil ---@field valid fun(self: CopilotChat.Chat) ---@field visible fun(self: CopilotChat.Chat) ---@field active fun(self: CopilotChat.Chat) @@ -30,6 +33,28 @@ function CopilotChatFoldExpr(lnum, separator) return '=' end +---@return string?, number?, number? +local function match_header(header) + if not header then + return + end + + local header_filename, header_start_line, header_end_line = + header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') + if not header_filename then + header_filename, header_start_line, header_end_line = + header:match('%[file:(.+)%] line:(%d+)-(%d+)') + end + + if header_filename then + header_filename = vim.fn.fnamemodify(header_filename, ':p:.') + header_start_line = tonumber(header_start_line) or 1 + header_end_line = tonumber(header_end_line) or header_start_line + end + + return header_filename, header_start_line, header_end_line +end + local Chat = class(function(self, help, on_buf_create) self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') self.name = 'copilot-chat' @@ -38,11 +63,16 @@ local Chat = class(function(self, help, on_buf_create) self.bufnr = nil self.winnr = nil self.spinner = nil - self.separator = nil + self.sections = {} + + -- Config + self.layout = nil self.auto_insert = false self.auto_follow_cursor = true self.highlight_headers = true - self.layout = nil + self.question_header = nil + self.answer_header = nil + self.separator = nil vim.treesitter.language.register('markdown', self.name) @@ -54,10 +84,10 @@ local Chat = class(function(self, help, on_buf_create) vim.bo[bufnr].textwidth = 0 vim.bo[bufnr].modifiable = false - vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, { + vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { buffer = bufnr, callback = function() - vim.defer_fn(function() + utils.debounce(self.name, function() self:render() end, 100) end, @@ -80,14 +110,48 @@ function Chat:visible() end function Chat:render() - if not self.highlight_headers or not self:visible() then - return - end - vim.api.nvim_buf_clear_namespace(self.bufnr, self.header_ns, 0, -1) local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) + local line_count = #lines + + local sections = {} + local current_section = nil + local current_block = nil + for l, line in ipairs(lines) do - if line:match(self.separator .. '$') then + local separator_found = false + + if line:match(self.answer_header .. self.separator .. '$') then + separator_found = true + if current_section then + current_section.end_line = l - 1 + table.insert(sections, current_section) + end + current_section = { + answer = true, + start_line = l + 1, + blocks = {}, + } + elseif line:match(self.question_header .. self.separator .. '$') then + separator_found = true + if current_section then + current_section.end_line = l - 1 + table.insert(sections, current_section) + end + current_section = { + answer = false, + start_line = l + 1, + blocks = {}, + } + elseif l == line_count then + if current_section then + current_section.end_line = l + table.insert(sections, current_section) + end + end + + -- Highlight separators + if self.highlight_headers and separator_found then local sep = vim.fn.strwidth(line) - vim.fn.strwidth(self.separator) -- separator line vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, sep, { @@ -104,7 +168,109 @@ function Chat:render() strict = false, }) end + + -- Parse code blocks + if current_section and current_section.answer then + local filetype = line:match('^```(%w+)$') + if filetype and not current_block then + local filename, start_line, end_line = match_header(lines[l - 1]) + if not filename then + filename, start_line, end_line = match_header(lines[l - 2]) + end + filename = filename or 'code-block' + + current_block = { + header = { + filename = filename, + start_line = start_line, + end_line = end_line, + filetype = filetype, + }, + start_line = l + 1, + } + elseif line == '```' and current_block then + current_block.end_line = l - 1 + table.insert(current_section.blocks, current_block) + current_block = nil + end + end end + + self.sections = sections +end + +function Chat:get_closest_section() + if not self:visible() then + return nil + end + + local cursor_pos = vim.api.nvim_win_get_cursor(self.winnr) + local cursor_line = cursor_pos[1] + local closest_section = nil + local max_line_below_cursor = -1 + + for _, section in ipairs(self.sections) do + if section.start_line <= cursor_line and section.start_line > max_line_below_cursor then + max_line_below_cursor = section.start_line + closest_section = section + end + end + + if not closest_section then + return nil + end + + local section_content = vim.api.nvim_buf_get_lines( + self.bufnr, + closest_section.start_line - 1, + closest_section.end_line, + false + ) + + return { + answer = closest_section.answer, + start_line = closest_section.start_line, + end_line = closest_section.end_line, + content = table.concat(section_content, '\n'), + } +end + +function Chat:get_closest_block() + if not self:visible() then + return nil + end + + local cursor_pos = vim.api.nvim_win_get_cursor(self.winnr) + local cursor_line = cursor_pos[1] + local closest_block = nil + local max_line_below_cursor = -1 + + for _, section in pairs(self.sections) do + for _, block in ipairs(section.blocks) do + if block.start_line <= cursor_line and block.start_line > max_line_below_cursor then + max_line_below_cursor = block.start_line + closest_block = block + end + end + end + + if not closest_block then + return nil + end + + local block_content = vim.api.nvim_buf_get_lines( + self.bufnr, + closest_block.start_line - 1, + closest_block.end_line, + false + ) + + return { + header = closest_block.header, + start_line = closest_block.start_line, + end_line = closest_block.end_line, + content = table.concat(block_content, '\n'), + } end function Chat:active() @@ -229,10 +395,12 @@ function Chat:open(config) end self.layout = layout - self.separator = config.separator self.auto_insert = config.auto_insert self.auto_follow_cursor = config.auto_follow_cursor self.highlight_headers = config.highlight_headers + self.question_header = config.question_header + self.answer_header = config.answer_header + self.separator = config.separator vim.wo[self.winnr].wrap = true vim.wo[self.winnr].linebreak = true diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f1baed20..766c6fe3 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -62,38 +62,6 @@ local function get_selection(config) return nil end ----@param prompt string ----@param system_prompt string ----@return string, string -local function update_prompts(prompt, system_prompt) - local prompts_to_use = M.prompts() - local try_again = false - local result = string.gsub(prompt, [[/[%w_]+]], function(match) - local found = prompts_to_use[string.sub(match, 2)] - if found then - if found.kind == 'user' then - local out = found.prompt - if out and string.match(out, [[/[%w_]+]]) then - try_again = true - end - system_prompt = found.system_prompt or system_prompt - return out - elseif found.kind == 'system' then - system_prompt = found.prompt - return '' - end - end - - return match - end) - - if try_again then - return update_prompts(result, system_prompt) - end - - return system_prompt, result -end - --- Highlights the selection in the source buffer. ---@param clear? boolean local function highlight_selection(clear) @@ -131,47 +99,17 @@ local function update_selection() highlight_selection() end ----@return string?, number?, number? -local function match_header(header) - if not header then - return - end - - local header_filename, header_start_line, header_end_line = - header:match('%[file:.+%]%((.+)%) line:(%d+)-(%d+)') - if not header_filename then - header_filename, header_start_line, header_end_line = - header:match('%[file:(.+)%] line:(%d+)-(%d+)') - end - - if header_filename then - header_filename = vim.fn.fnamemodify(header_filename, ':p:.') - header_start_line = tonumber(header_start_line) or 1 - header_end_line = tonumber(header_end_line) or header_start_line - end - - return header_filename, header_start_line, header_end_line -end - ---@return CopilotChat.Diff.diff|nil local function get_diff() - local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) - local current_line = vim.api.nvim_win_get_cursor(0)[1] - local section, section_start = - utils.find_lines(chat_lines, current_line, M.config.separator .. '$') - local change, change_start = - utils.find_lines(section, current_line - section_start, '^```%w+$', '^```$') - - -- If no change block found, return nil - if not change or #change == 0 then + local block = state.chat:get_closest_block() + + -- If no block found, return nil + if not block then return nil end - -- Try to get header info first - local header = section[change_start - 2] - local header_filename, header_start_line, header_end_line = match_header(header) - -- Initialize variables with selection if available + local header = block.header local selection = get_selection(state.config) local reference = selection and selection.content local start_line = selection and selection.start_line @@ -181,21 +119,21 @@ local function get_diff() local bufnr = selection and selection.bufnr -- If we have header info, use it as source of truth - if header_filename and header_start_line and header_end_line then + if header.start_line and header.end_line then -- Try to find matching buffer and window bufnr = nil for _, win in ipairs(vim.api.nvim_list_wins()) do local win_buf = vim.api.nvim_win_get_buf(win) - if utils.filename_same(vim.api.nvim_buf_get_name(win_buf), header_filename) then + if utils.filename_same(vim.api.nvim_buf_get_name(win_buf), header.filename) then bufnr = win_buf break end end - filename = header_filename - filetype = vim.filetype.match({ filename = filename }) - start_line = header_start_line - end_line = header_end_line + filename = header.filename + filetype = header.filetype or vim.filetype.match({ filename = filename }) + start_line = header.start_line + end_line = header.end_line -- If we found a valid buffer, get the reference content if bufnr and utils.buf_valid(bufnr) then @@ -211,7 +149,7 @@ local function get_diff() end return { - change = table.concat(change, '\n'), + change = block.content, reference = reference or '', filename = filename or 'unknown', filetype = filetype or 'text', @@ -221,6 +159,17 @@ local function get_diff() } end +---@param winnr number +---@param bufnr number +---@param start_line number +---@param end_line number +local function jump_to_diff(winnr, bufnr, start_line, end_line) + vim.api.nvim_win_set_cursor(winnr, { start_line, 0 }) + pcall(vim.api.nvim_buf_set_mark, bufnr, '<', start_line, 0, {}) + pcall(vim.api.nvim_buf_set_mark, bufnr, '>', end_line, 0, {}) + update_selection() +end + ---@param diff CopilotChat.Diff.diff? local function apply_diff(diff) if not diff or not diff.bufnr then @@ -233,17 +182,46 @@ local function apply_diff(diff) end local lines = vim.split(diff.change, '\n', { trimempty = false }) - - -- Update the source buffer with the change vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) + jump_to_diff(winnr, diff.bufnr, diff.start_line, diff.start_line + #lines - 1) +end - -- Update visual selection marks to the diff start/end and move cursor - vim.api.nvim_win_set_cursor(winnr, { diff.start_line, 0 }) - vim.api.nvim_buf_set_mark(diff.bufnr, '<', diff.start_line, 0, {}) - vim.api.nvim_buf_set_mark(diff.bufnr, '>', diff.start_line + #lines - 1, 0, {}) - update_selection() +---@param prompt string +---@param system_prompt string +---@return string, string +local function update_prompts(prompt, system_prompt) + local prompts_to_use = M.prompts() + local try_again = false + local result = string.gsub(prompt, [[/[%w_]+]], function(match) + local found = prompts_to_use[string.sub(match, 2)] + if found then + if found.kind == 'user' then + local out = found.prompt + if out and string.match(out, [[/[%w_]+]]) then + try_again = true + end + system_prompt = found.system_prompt or system_prompt + return out + elseif found.kind == 'system' then + system_prompt = found.prompt + return '' + end + end + + return match + end) + + if try_again then + return update_prompts(result, system_prompt) + end + + return system_prompt, result end +---@param config CopilotChat.config +---@param message string? +---@param hide_help boolean? +---@param start_of_chat boolean? local function finish(config, message, hide_help, start_of_chat) if config.no_chat then return @@ -279,6 +257,9 @@ local function finish(config, message, hide_help, start_of_chat) end end +---@param config CopilotChat.config +---@param err string|table +---@param append_newline boolean? local function show_error(config, err, append_newline) log.error(vim.inspect(err)) @@ -633,17 +614,20 @@ function M.ask(prompt, config) finish(config, nil, true) end - state.last_prompt = prompt - - -- Clear the current input prompt before asking a new question - local chat_lines = vim.api.nvim_buf_get_lines(state.chat.bufnr, 0, -1, false) - local _, start_line, end_line = - utils.find_lines(chat_lines, #chat_lines, M.config.separator .. '$', nil, true) - if #chat_lines == end_line then - vim.api.nvim_buf_set_lines(state.chat.bufnr, start_line, end_line, false, { '' }) + local section = state.chat:get_closest_section() + if not section or section.answer then + return end - state.chat:append(prompt) + state.last_prompt = prompt + vim.api.nvim_buf_set_lines( + state.chat.bufnr, + section.start_line - 1, + section.end_line, + false, + {} + ) + state.chat:append('\n\n' .. prompt) state.chat:append('\n\n' .. config.answer_header .. config.separator .. '\n\n') end @@ -1026,14 +1010,20 @@ function M.setup(config) map_key(M.config.mappings.complete, bufnr, trigger_complete) map_key(M.config.mappings.submit_prompt, bufnr, function() - local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local current_line = vim.api.nvim_win_get_cursor(0)[1] - local lines = - utils.find_lines(chat_lines, current_line, M.config.separator .. '$', nil, true) - M.ask(vim.trim(table.concat(lines, '\n')), state.config) + local section = state.chat:get_closest_section() + if not section or section.answer then + return + end + + M.ask(section.content, state.config) end) map_key(M.config.mappings.toggle_sticky, bufnr, function() + local section = state.chat:get_closest_section() + if not section or section.answer then + return + end + local current_line = vim.trim(vim.api.nvim_get_current_line()) if current_line == '' then return @@ -1043,37 +1033,33 @@ function M.setup(config) local cur_line = cursor[1] vim.api.nvim_buf_set_lines(bufnr, cur_line - 1, cur_line, false, {}) - local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local _, start_line, end_line = - utils.find_lines(chat_lines, cur_line, M.config.separator .. '$', nil, true) - if vim.startswith(current_line, '> ') then return end - if start_line then - local insert_line = start_line - local first_one = true - - for i = insert_line, end_line do - local line = chat_lines[i] - if line and vim.trim(line) ~= '' then - if vim.startswith(line, '> ') then - first_one = false - else - break - end - elseif i >= start_line + 1 then + local lines = vim.split(section.content, '\n') + local insert_line = 1 + local first_one = true + + for i = insert_line, #lines do + local line = lines[i] + if line and vim.trim(line) ~= '' then + if vim.startswith(line, '> ') then + first_one = false + else break end - - insert_line = insert_line + 1 + elseif i >= 2 then + break end - local lines = first_one and { '> ' .. current_line, '' } or { '> ' .. current_line } - vim.api.nvim_buf_set_lines(bufnr, insert_line - 1, insert_line - 1, false, lines) - vim.api.nvim_win_set_cursor(0, cursor) + insert_line = insert_line + 1 end + + insert_line = section.start_line + insert_line - 1 + local to_insert = first_one and { '> ' .. current_line, '' } or { '> ' .. current_line } + vim.api.nvim_buf_set_lines(bufnr, insert_line - 1, insert_line - 1, false, to_insert) + vim.api.nvim_win_set_cursor(0, cursor) end) map_key(M.config.mappings.accept_diff, bufnr, function() @@ -1113,84 +1099,35 @@ function M.setup(config) vim.bo[diff_bufnr].filetype = diff.filetype end - -- Open the buffer in the source window and move cursor vim.api.nvim_win_set_buf(state.source.winnr, diff_bufnr) - vim.api.nvim_win_set_cursor(state.source.winnr, { diff.start_line, 0 }) - - -- Set the marks for visual selection and update selection - pcall(vim.api.nvim_buf_set_mark, diff_bufnr, '<', diff.start_line, 0, {}) - pcall(vim.api.nvim_buf_set_mark, diff_bufnr, '>', diff.end_line, 0, {}) - update_selection() + jump_to_diff(state.source.winnr, diff_bufnr, diff.start_line, diff.end_line) end) map_key(M.config.mappings.quickfix_diffs, bufnr, function() - local chat_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local selection = get_selection(state.config) local items = {} - local in_block = false - local block_start = 0 - local filetype = '' - local in_answer = false - local last_header = nil - - for i, line in ipairs(chat_lines) do - -- Track if we're in an AI response section - if line:match(M.config.answer_header .. M.config.separator .. '$') then - in_answer = true - elseif line:match(M.config.question_header .. M.config.separator .. '$') then - in_answer = false - end - -- Only process code blocks in AI responses - if in_answer then - -- Try to capture markdown header with file info - local filename, start_line, end_line = match_header(line) - if filename then - last_header = { - filename = filename, - start_line = start_line, - end_line = end_line, - } + for _, section in ipairs(state.chat.sections) do + for _, block in ipairs(section.blocks) do + local header = block.header + + if not header.start_line and selection then + header.filename = selection.filename .. ' (selection)' + header.start_line = selection.start_line + header.end_line = selection.end_line end - if line:match('^```%w+$') then - in_block = true - block_start = i + 1 - filetype = line:match('^```(%w+)$') - elseif line == '```' and in_block then - in_block = false - local item = { - bufnr = bufnr, - lnum = block_start, - end_lnum = i - 1, - } - - if last_header then - item.text = string.format( - '%s [lines %d-%d]', - last_header.filename, - last_header.start_line, - last_header.end_line - ) - elseif - selection - and selection.filename - and selection.start_line - and selection.end_line - then - item.text = string.format( - '%s [lines %d-%d]', - selection.filename, - selection.start_line, - selection.end_line - ) - else - item.text = string.format('Code block (%s)', filetype) - end - - table.insert(items, item) - last_header = nil + local text = string.format('%s (%s)', header.filename, header.filetype) + if header.start_line and header.end_line then + text = text .. string.format(' [lines %d-%d]', header.start_line, header.end_line) end + + table.insert(items, { + bufnr = bufnr, + lnum = block.start_line, + end_lnum = block.end_line, + text = text, + }) end end @@ -1268,7 +1205,7 @@ function M.setup(config) local char = line:sub(col, col) if vim.tbl_contains(M.complete_info().triggers, char) then - utils.debounce(trigger_complete, 100) + utils.debounce('complete', trigger_complete, 100) end end, }) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index b1e921de..dd15ab96 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,4 +1,5 @@ local M = {} +M.timers = {} --- Create class ---@param fn function The class constructor @@ -89,62 +90,6 @@ function M.blend_color(color_name, blend) return string.format('#%02x%02x%02x', r, g, b) end ---- Find lines between two patterns ----@param lines table The lines to search ----@param current_line number The current line ----@param start_pattern string The start pattern ----@param end_pattern string? The end pattern ----@param allow_end_of_file boolean? Allow end of file as end pattern -function M.find_lines(lines, current_line, start_pattern, end_pattern, allow_end_of_file) - if not end_pattern then - end_pattern = start_pattern - end - - local line_count = #lines - local separator_line_start = 1 - local separator_line_finish = line_count - local found_one = false - - -- Find starting separator line - for i = current_line, 1, -1 do - local line = lines[i] - - if line and string.match(line, start_pattern) then - separator_line_start = i + 1 - - for x = separator_line_start, line_count do - local next_line = lines[x] - if next_line and string.match(next_line, end_pattern) then - separator_line_finish = x - 1 - found_one = true - break - end - if allow_end_of_file and x == line_count then - separator_line_finish = x - found_one = true - break - end - end - - if found_one then - break - end - end - end - - if not found_one then - return {}, 1, 1 - end - - -- Extract everything between the last and next separator or end of file - local result = {} - for i = separator_line_start, separator_line_finish do - table.insert(result, lines[i]) - end - - return result, separator_line_start, separator_line_finish -end - --- Return to normal mode function M.return_to_normal_mode() local mode = vim.fn.mode():lower() @@ -161,11 +106,12 @@ function M.deprecate(old, new) end --- Debounce a function -function M.debounce(fn, delay) - if M.timer then - M.timer:stop() +function M.debounce(id, fn, delay) + if M.timers[id] then + M.timers[id]:stop() + M.timers[id] = nil end - M.timer = vim.defer_fn(fn, delay) + M.timers[id] = vim.defer_fn(fn, delay) end --- Check if a buffer is valid From fb8c93f8cce6d9acea33e6959221c80366862667 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 10:28:55 +0100 Subject: [PATCH 232/338] feat: add register context and improve selection handling Adds new register context that allows accessing vim register contents in chat and improves selection handling by: - Making selection fields required instead of optional - Improving unnamed register selection to include buffer context - Deprecating clipboard selection in favor of register context - Adding proper mark setting for better selection tracking - Supporting array of default contexts in config The register context supports all standard vim registers and provides a UI for selecting them when used. This makes accessing clipboard and other registers more intuitive. Signed-off-by: Tomas Slusny --- README.md | 50 +++++++++++++++------------------- lua/CopilotChat/config.lua | 52 +++++++++++++++++++++++++++++++----- lua/CopilotChat/context.lua | 17 ++++++++++++ lua/CopilotChat/init.lua | 22 ++++++++++----- lua/CopilotChat/select.lua | 53 ++++++++++++++++++++----------------- 5 files changed, 127 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 5614cf2b..3a25dcc2 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ What is 1 + 11 ### Models You can list available models with `:CopilotChatModels` command. Model determines the AI model used for the chat. -You can set the model in the prompt by using `$` followed by the model name. +You can set the model in the prompt by using `$` followed by the model name or default model via config using `model` key. Default models are: - `gpt-4o` - This is the default Copilot Chat model. It is a versatile, multimodal model that excels in both text and image processing and is designed to provide fast, reliable responses. It also has superior performance in non-English languages. Gpt-4o is hosted on Azure. @@ -254,7 +254,7 @@ You can use more models from [here](https://github.com/marketplace/models) by us ### Agents Agents are used to determine the AI agent used for the chat. You can list available agents with `:CopilotChatAgents` command. -You can set the agent in the prompt by using `@` followed by the agent name. +You can set the agent in the prompt by using `@` followed by the agent name or default agent via config using `agent` key. Default "noop" agent is `copilot`. For more information about extension agents, see [here](https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat) @@ -263,7 +263,8 @@ You can install more agents from [here](https://github.com/marketplace?type=apps ### Contexts Contexts are used to determine the context of the chat. -You can add context to the prompt by using `#` followed by the context name (multiple contexts are supported). +You can add context to the prompt by using `#` followed by the context name or default context via config using `context` (can be single or array) key. +Any amount of context can be added to the prompt. If context supports input, you can set the input in the prompt by using `:` followed by the input (or pressing `complete` key after `:`). Default contexts are: @@ -272,6 +273,7 @@ Default contexts are: - `file` - Includes content of provided file in chat context. Supports input. - `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. - `git` - Includes current git diff in chat context (default unstaged). Supports input. +- `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. You can define custom contexts like this: @@ -317,33 +319,15 @@ What is my birthday Selections are used to determine the source of the chat (so basically what to chat about). Selections are configurable either by default or by prompt. Default selection is `visual` or `buffer` (if no visual selection). -Default supported selections that live in `local select = require("CopilotChat.select")` are: +Selection includes content, start and end position, buffer info and diagnostic info (if available). +Supported selections that live in `local select = require("CopilotChat.select")` are: -- `select.visual` - Current visual selection. Includes diagnostic in selection. Works well with diffs. -- `select.buffer` - Current buffer content. Includes diagnostic in selection. Works well with diffs. -- `select.line` - Current line content. Includes diagnostic in selection. Works decently with diffs. -- `select.unnamed` - Content from the unnamed register. -- `select.clipboard` - Content from system clipboard. +- `select.visual` - Current visual selection. +- `select.buffer` - Current buffer content. +- `select.line` - Current line content. +- `select.unnamed` - Unnamed register content. This register contains last deleted, changed or yanked content. -You can define custom selection functions like this: - -```lua -{ - selection = function() - -- Get content from * register - local content = vim.fn.getreg('*') - if not content or content == '' then - return nil - end - - return { - content = content, - } - end -} -``` - -Or chain multiple selections like this: +You can chain multiple selections like this: ```lua { @@ -394,6 +378,11 @@ chat.ask("Explain how it works.", { selection = require("CopilotChat.select").buffer, }) +-- Ask a question and provide custom contexts +chat.ask("Explain how it works.", { + context = { 'buffers', 'files', 'register:+' }, +}) + -- Ask a question and do something with the response chat.ask("Show me something interesting", { callback = function(response) @@ -435,7 +424,7 @@ Also see [here](/lua/CopilotChat/config.lua): system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use (can be specified manually in prompt via #). + context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions @@ -479,6 +468,9 @@ Also see [here](/lua/CopilotChat/config.lua): git = { -- see config.lua for implementation }, + register = { + -- see config.lua for implementation + }, }, -- default prompts diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 835c9705..bc6cd1c3 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -15,11 +15,11 @@ local utils = require('CopilotChat.utils') ---@class CopilotChat.config.selection ---@field content string ----@field start_line number? ----@field end_line number? ----@field filename string? ----@field filetype string? ----@field bufnr number? +---@field start_line number +---@field end_line number +---@field filename string +---@field filetype string +---@field bufnr number ---@field diagnostics table? ---@class CopilotChat.config.context @@ -77,7 +77,7 @@ local utils = require('CopilotChat.utils') ---@field system_prompt string? ---@field model string? ---@field agent string? ----@field context string? +---@field context string|table|nil ---@field temperature number? ---@field question_header string? ---@field answer_header string? @@ -108,7 +108,7 @@ return { system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use (can be specified manually in prompt via #). + context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions @@ -227,6 +227,44 @@ return { } end, }, + register = { + description = 'Includes contents of register in chat context (default +, e.g clipboard). Supports input.', + input = function(callback) + local registers = { + ['+'] = 'synchronized with the system clipboard', + ['*'] = 'synchronized with the selection clipboard', + ['"'] = 'last deleted, changed, or yanked content', + ['0'] = 'last yank', + ['-'] = 'deleted or changed content smaller than one line', + ['.'] = 'last inserted text', + ['%'] = 'name of the current file', + [':'] = 'most recent executed command', + ['#'] = 'alternate buffer', + ['='] = 'result of an expression', + ['/'] = 'last search pattern', + } + + vim.ui.select( + vim.tbl_map(function(k) + return { id = k, name = k .. ' - ' .. (registers[k] or '') } + end, vim.tbl_keys(registers)), + { + prompt = 'Select a register> ', + format_item = function(item) + return item.name + end, + }, + function(choice) + callback(choice and choice.id) + end + ) + end, + resolve = function(input) + return { + context.register(input), + } + end, + }, }, -- default prompts diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index f751896f..d77bc40a 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -303,6 +303,23 @@ function M.gitdiff(type, bufnr) } end +--- Return contents of specified register +---@param register string? +---@return CopilotChat.copilot.embed? +function M.register(register) + register = register or '+' + local lines = vim.fn.getreg(register) + if not lines or lines == '' then + return nil + end + + return { + content = lines, + filename = 'vim_register_' .. register, + filetype = '', + } +end + --- Filter embeddings based on the query ---@param copilot CopilotChat.Copilot ---@param prompt string diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 766c6fe3..ffcc1161 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -75,7 +75,7 @@ local function highlight_selection(clear) end local selection = get_selection(state.config) - if not selection or not selection.start_line or not utils.buf_valid(selection.bufnr) then + if not selection or not utils.buf_valid(selection.bufnr) then return end @@ -143,16 +143,16 @@ local function get_diff() end end - -- If we don't have either selection or valid header info, we can't proceed - if not start_line or not end_line then + -- If we are missing info, there is no diff to be made + if not start_line or not end_line or not filename then return nil end return { change = block.content, reference = reference or '', - filename = filename or 'unknown', - filetype = filetype or 'text', + filetype = filetype or '', + filename = filename, start_line = start_line, end_line = end_line, bufnr = bufnr, @@ -167,6 +167,8 @@ local function jump_to_diff(winnr, bufnr, start_line, end_line) vim.api.nvim_win_set_cursor(winnr, { start_line, 0 }) pcall(vim.api.nvim_buf_set_mark, bufnr, '<', start_line, 0, {}) pcall(vim.api.nvim_buf_set_mark, bufnr, '>', end_line, 0, {}) + pcall(vim.api.nvim_buf_set_mark, bufnr, '[', start_line, 0, {}) + pcall(vim.api.nvim_buf_set_mark, bufnr, ']', end_line, 0, {}) update_selection() end @@ -666,7 +668,13 @@ function M.ask(prompt, config) -- Sort and parse contexts local contexts = {} if config.context then - table.insert(contexts, config.context) + if type(config.context) == 'table' then + for _, config_context in ipairs(config.context) do + table.insert(contexts, config_context) + end + else + table.insert(contexts, config.context) + end end for prompt_context in prompt:gmatch('#([^%s]+)') do table.insert(contexts, prompt_context) @@ -1164,7 +1172,7 @@ function M.setup(config) map_key(M.config.mappings.show_user_selection, bufnr, function() local selection = get_selection(state.config) - if not selection or not selection.content then + if not selection then return end diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 89fd8e08..5eef4ecf 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -37,21 +37,19 @@ end --- @return CopilotChat.config.selection|nil function M.visual(source) local bufnr = source.bufnr - local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '<')) local finish_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '>')) - - -- Swap positions if selection is backwards + if start_line == 0 or finish_line == 0 then + return nil + end if start_line > finish_line then start_line, finish_line = finish_line, start_line end - -- Get selected text local ok, lines = pcall(vim.api.nvim_buf_get_lines, bufnr, start_line - 1, finish_line, false) if not ok then return nil end - local lines_content = table.concat(lines, '\n') if vim.trim(lines_content) == '' then return nil @@ -74,7 +72,6 @@ end function M.buffer(source) local bufnr = source.bufnr local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - if not lines or #lines == 0 then return nil end @@ -100,7 +97,6 @@ function M.line(source) local winnr = source.winnr local cursor = vim.api.nvim_win_get_cursor(winnr) local line = vim.api.nvim_buf_get_lines(bufnr, cursor[1] - 1, cursor[1], false)[1] - if not line then return nil end @@ -119,33 +115,42 @@ function M.line(source) end --- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. +--- @param source CopilotChat.config.source --- @return CopilotChat.config.selection|nil -function M.unnamed() - local lines = vim.fn.getreg('"') +function M.unnamed(source) + local bufnr = source.bufnr + local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '[')) + local finish_line = unpack(vim.api.nvim_buf_get_mark(bufnr, ']')) + if start_line == 0 or finish_line == 0 then + return nil + end + if start_line > finish_line then + start_line, finish_line = finish_line, start_line + end - if not lines or lines == '' then + local ok, lines = pcall(vim.api.nvim_buf_get_lines, bufnr, start_line - 1, finish_line, false) + if not ok then + return nil + end + local lines_content = table.concat(lines, '\n') + if vim.trim(lines_content) == '' then return nil end return { - content = lines, - filename = 'unnamed', + content = lines_content, + filename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ':p:.'), + filetype = vim.bo[bufnr].filetype, + start_line = start_line, + end_line = finish_line, + bufnr = bufnr, + diagnostics = get_diagnostics_in_range(bufnr, start_line, finish_line), } end ---- Select and process contents of plus register (+). This register is synchronized with system clipboard. ---- @return CopilotChat.config.selection|nil function M.clipboard() - local lines = vim.fn.getreg('+') - - if not lines or lines == '' then - return nil - end - - return { - content = lines, - filename = 'clipboard', - } + utils.deprecated('selection.clipboard', 'context.register:+') + return nil end function M.gitdiff() From eb22ba36fe56406fefbb35882fac5c8adc6eee8f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 Nov 2024 10:21:52 +0000 Subject: [PATCH 233/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 61 ++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index fd6c6b8a..36aaa25c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -277,7 +277,8 @@ MODELS ~ You can list available models with `:CopilotChatModels` command. Model determines the AI model used for the chat. You can set the model in the prompt -by using `$` followed by the model name. Default models are: +by using `$` followed by the model name or default model via config using +`model` key. Default models are: - `gpt-4o` - This is the default Copilot Chat model. It is a versatile, multimodal model that excels in both text and image processing and is designed to provide fast, reliable responses. It also has superior performance in non-English languages. Gpt-4o is hosted on Azure. - `claude-3.5-sonnet` - This model excels at coding tasks across the entire software development lifecycle, from initial design to bug fixes, maintenance to optimizations. GitHub Copilot uses Claude 3.5 Sonnet hosted on Amazon Web Services. @@ -295,8 +296,8 @@ AGENTS ~ Agents are used to determine the AI agent used for the chat. You can list available agents with `:CopilotChatAgents` command. You can set the agent in -the prompt by using `@` followed by the agent name. Default "noop" agent is -`copilot`. +the prompt by using `@` followed by the agent name or default agent via config +using `agent` key. Default "noop" agent is `copilot`. For more information about extension agents, see here @@ -307,16 +308,18 @@ You can install more agents from here CONTEXTS ~ Contexts are used to determine the context of the chat. You can add context to -the prompt by using `#` followed by the context name (multiple contexts are -supported). If context supports input, you can set the input in the prompt by -using `:` followed by the input (or pressing `complete` key after `:`). Default -contexts are: +the prompt by using `#` followed by the context name or default context via +config using `context` (can be single or array) key. Any amount of context can +be added to the prompt. If context supports input, you can set the input in the +prompt by using `:` followed by the input (or pressing `complete` key after +`:`). Default contexts are: - `buffer` - Includes specified buffer in chat context (default current). Supports input. - `buffers` - Includes all buffers in chat context (default listed). Supports input. - `file` - Includes content of provided file in chat context. Supports input. - `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. - `git` - Includes current git diff in chat context (default unstaged). Supports input. +- `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. You can define custom contexts like this: @@ -362,35 +365,17 @@ SELECTIONS ~ Selections are used to determine the source of the chat (so basically what to chat about). Selections are configurable either by default or by prompt. -Default selection is `visual` or `buffer` (if no visual selection). Default -supported selections that live in `local select = +Default selection is `visual` or `buffer` (if no visual selection). Selection +includes content, start and end position, buffer info and diagnostic info (if +available). Supported selections that live in `local select = require("CopilotChat.select")` are: -- `select.visual` - Current visual selection. Includes diagnostic in selection. Works well with diffs. -- `select.buffer` - Current buffer content. Includes diagnostic in selection. Works well with diffs. -- `select.line` - Current line content. Includes diagnostic in selection. Works decently with diffs. -- `select.unnamed` - Content from the unnamed register. -- `select.clipboard` - Content from system clipboard. +- `select.visual` - Current visual selection. +- `select.buffer` - Current buffer content. +- `select.line` - Current line content. +- `select.unnamed` - Unnamed register content. This register contains last deleted, changed or yanked content. -You can define custom selection functions like this: - ->lua - { - selection = function() - -- Get content from * register - local content = vim.fn.getreg('*') - if not content or content == '' then - return nil - end - - return { - content = content, - } - end - } -< - -Or chain multiple selections like this: +You can chain multiple selections like this: >lua { @@ -442,6 +427,11 @@ API ~ selection = require("CopilotChat.select").buffer, }) + -- Ask a question and provide custom contexts + chat.ask("Explain how it works.", { + context = { 'buffers', 'files', 'register:+' }, + }) + -- Ask a question and do something with the response chat.ask("Show me something interesting", { callback = function(response) @@ -485,7 +475,7 @@ Also see here : system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context to use (can be specified manually in prompt via #). + context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature question_header = '## User ', -- Header to use for user questions @@ -529,6 +519,9 @@ Also see here : git = { -- see config.lua for implementation }, + register = { + -- see config.lua for implementation + }, }, -- default prompts From 9f3366f890dc71d4d68d36938624ebfbd77e8db1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 12:26:44 +0100 Subject: [PATCH 234/338] refactor: improve class extending system Add extendable constructor and move overlay create logic to :create Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 52 ++++++++++++++++--------------------- lua/CopilotChat/diff.lua | 12 +-------- lua/CopilotChat/overlay.lua | 25 ++++++++++-------- lua/CopilotChat/utils.lua | 4 +++ 4 files changed, 42 insertions(+), 51 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index bd4aeb44..ccc10c7f 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -56,11 +56,10 @@ local function match_header(header) end local Chat = class(function(self, help, on_buf_create) + Overlay.init(self, 'copilot-chat', help, on_buf_create) + vim.treesitter.language.register('markdown', self.name) + self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') - self.name = 'copilot-chat' - self.help = help - self.on_buf_create = on_buf_create - self.bufnr = nil self.winnr = nil self.spinner = nil self.sections = {} @@ -73,35 +72,30 @@ local Chat = class(function(self, help, on_buf_create) self.question_header = nil self.answer_header = nil self.separator = nil +end, Overlay) - vim.treesitter.language.register('markdown', self.name) +function Chat:create() + local bufnr = Overlay.create(self) + vim.bo[bufnr].syntax = 'markdown' + vim.bo[bufnr].textwidth = 0 - self.buf_create = function() - local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr, self.name) - vim.bo[bufnr].filetype = self.name - vim.bo[bufnr].syntax = 'markdown' - vim.bo[bufnr].textwidth = 0 - vim.bo[bufnr].modifiable = false - - vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { - buffer = bufnr, - callback = function() - utils.debounce(self.name, function() - self:render() - end, 100) - end, - }) - - if not self.spinner then - self.spinner = Spinner(bufnr) - else - self.spinner.bufnr = bufnr - end + vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { + buffer = bufnr, + callback = function() + utils.debounce(self.name, function() + self:render() + end, 100) + end, + }) - return bufnr + if not self.spinner then + self.spinner = Spinner(bufnr) + else + self.spinner.bufnr = bufnr end -end, Overlay) + + return bufnr +end function Chat:visible() return self.winnr diff --git a/lua/CopilotChat/diff.lua b/lua/CopilotChat/diff.lua index f8ed47fe..2ee24bc3 100644 --- a/lua/CopilotChat/diff.lua +++ b/lua/CopilotChat/diff.lua @@ -19,23 +19,13 @@ local utils = require('CopilotChat.utils') local class = utils.class local Diff = class(function(self, help, on_buf_create) + Overlay.init(self, 'copilot-diff', help, on_buf_create) self.hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights') vim.api.nvim_set_hl(self.hl_ns, '@diff.plus', { bg = utils.blend_color('DiffAdd', 20) }) vim.api.nvim_set_hl(self.hl_ns, '@diff.minus', { bg = utils.blend_color('DiffDelete', 20) }) vim.api.nvim_set_hl(self.hl_ns, '@diff.delta', { bg = utils.blend_color('DiffChange', 20) }) - self.name = 'copilot-diff' - self.help = help - self.on_buf_create = on_buf_create - self.bufnr = nil self.diff = nil - - self.buf_create = function() - local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr, self.name) - vim.bo[bufnr].filetype = self.name - return bufnr - end end, Overlay) function Diff:show(diff, winnr) diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index e8411351..b3a7dfcc 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -1,5 +1,7 @@ ---@class CopilotChat.Overlay +---@field name string ---@field bufnr number +---@field create fun(self: CopilotChat.Overlay) ---@field valid fun(self: CopilotChat.Overlay) ---@field validate fun(self: CopilotChat.Overlay) ---@field show fun(self: CopilotChat.Overlay, text: string, filetype: string?, winnr: number) @@ -11,19 +13,21 @@ local utils = require('CopilotChat.utils') local class = utils.class local Overlay = class(function(self, name, help, on_buf_create) + self.name = name self.help = help + self.help_ns = vim.api.nvim_create_namespace('copilot-chat-help') self.on_buf_create = on_buf_create self.bufnr = nil - - self.buf_create = function() - local bufnr = vim.api.nvim_create_buf(false, true) - vim.bo[bufnr].filetype = name - vim.bo[bufnr].modifiable = false - vim.api.nvim_buf_set_name(bufnr, name) - return bufnr - end end) +function Overlay:create() + local bufnr = vim.api.nvim_create_buf(false, true) + vim.bo[bufnr].filetype = self.name + vim.bo[bufnr].modifiable = false + vim.api.nvim_buf_set_name(bufnr, self.name) + return bufnr +end + function Overlay:valid() return utils.buf_valid(self.bufnr) end @@ -33,7 +37,7 @@ function Overlay:validate() return end - self.bufnr = self.buf_create(self) + self.bufnr = self:create() self.on_buf_create(self.bufnr) end @@ -82,9 +86,8 @@ function Overlay:show_help(msg, offset) end self:validate() - local help_ns = vim.api.nvim_create_namespace('copilot-chat-help') local line = vim.api.nvim_buf_line_count(self.bufnr) + offset - vim.api.nvim_buf_set_extmark(self.bufnr, help_ns, math.max(0, line - 1), 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, self.help_ns, math.max(0, line - 1), 0, { id = 1, hl_mode = 'combine', priority = 100, diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index dd15ab96..02bdb3b1 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -27,6 +27,10 @@ function M.class(fn, parent) return self end + function out.init(self, ...) + fn(self, ...) + end + return out end From a3d24298b95d0aa6dd8e512406ec705b10852039 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 14:44:54 +0100 Subject: [PATCH 235/338] fix: improve selection validation checks Enhance selection validation by properly checking for existence of start_line and end_line values. This prevents potential nil access errors when handling invalid selections. Previously the code only checked if end_line was greater than 0, which could lead to errors if start_line or end_line were nil. --- lua/CopilotChat/copilot.lua | 2 +- lua/CopilotChat/init.lua | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 397e078b..71633291 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -161,7 +161,7 @@ local function generate_selection_messages(selection) local out = string.format('# FILE:%s CONTEXT\n', filename:upper()) out = out .. "User's active selection:\n" - if selection.start_line and selection.end_line > 0 then + if selection.start_line and selection.end_line then out = out .. string.format( 'Excerpt from %s, lines %s to %s:\n', diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ffcc1161..cca38511 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -75,7 +75,12 @@ local function highlight_selection(clear) end local selection = get_selection(state.config) - if not selection or not utils.buf_valid(selection.bufnr) then + if + not selection + or not utils.buf_valid(selection.bufnr) + or not selection.start_line + or not selection.end_line + then return end From d99aef328ccbdd56f1ed25550530391bf9472d1a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 16:05:23 +0100 Subject: [PATCH 236/338] feat: add show context command for Copilot Chat Add new command to show context of the current chat message that maps to `gc` by default. Unify existing overlay windows into single overlay implementation and improve resolution of prompts and embeddings by extracting them into separate functions. The overlay now also shows truncated preview of longer files. This change improves debugging experience when working with contextual prompts by allowing users to inspect what files and contexts are being used for the current prompt. --- README.md | 7 ++ lua/CopilotChat/config.lua | 4 + lua/CopilotChat/init.lua | 172 ++++++++++++++++++++---------------- lua/CopilotChat/overlay.lua | 4 + 4 files changed, 109 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 3a25dcc2..e82e13f9 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/ma - `gd` - Show diff between source and nearest diff - `gp` - Show system prompt for current chat - `gs` - Show current user selection +- `gc` - Show current user context - `gh` - Show help message The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: @@ -558,6 +559,12 @@ Also see [here](/lua/CopilotChat/config.lua): show_user_selection = { normal = 'gs' }, + show_user_context = { + normal = 'gc', + }, + show_help = { + normal = 'gh', + }, }, } ``` diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index bc6cd1c3..b59b20d5 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -66,6 +66,7 @@ local utils = require('CopilotChat.utils') ---@field show_diff CopilotChat.config.mapping? ---@field show_system_prompt CopilotChat.config.mapping? ---@field show_user_selection CopilotChat.config.mapping? +---@field show_user_context CopilotChat.config.mapping? ---@field show_help CopilotChat.config.mapping? --- CopilotChat default configuration @@ -390,6 +391,9 @@ return { show_user_selection = { normal = 'gs', }, + show_user_context = { + normal = 'gc', + }, show_help = { normal = 'gh', }, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index cca38511..45e2fe1b 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -21,8 +21,7 @@ local plugin_name = 'CopilotChat.nvim' --- @field last_response string? --- @field chat CopilotChat.Chat? --- @field diff CopilotChat.Diff? ---- @field system_prompt CopilotChat.Overlay? ---- @field user_selection CopilotChat.Overlay? +--- @field overlay CopilotChat.Overlay? --- @field help CopilotChat.Overlay? local state = { copilot = nil, @@ -38,9 +37,7 @@ local state = { -- Overlays chat = nil, diff = nil, - system_prompt = nil, - user_selection = nil, - help = nil, + overlay = nil, } ---@param config CopilotChat.config @@ -196,7 +193,7 @@ end ---@param prompt string ---@param system_prompt string ---@return string, string -local function update_prompts(prompt, system_prompt) +local function resolve_prompts(prompt, system_prompt) local prompts_to_use = M.prompts() local try_again = false local result = string.gsub(prompt, [[/[%w_]+]], function(match) @@ -219,12 +216,61 @@ local function update_prompts(prompt, system_prompt) end) if try_again then - return update_prompts(result, system_prompt) + return resolve_prompts(result, system_prompt) end return system_prompt, result end +---@param prompt string +---@param config CopilotChat.config +---@return table, string +local function resolve_embeddings(prompt, config) + local embedding_map = {} + local function parse_context(prompt_context) + local split = vim.split(prompt_context, ':') + local context_name = table.remove(split, 1) + local context_input = vim.trim(table.concat(split, ':')) + local context_value = config.contexts[context_name] + if context_input == '' then + context_input = nil + end + + if context_value then + for _, embedding in ipairs(context_value.resolve(context_input, state.source)) do + if embedding then + embedding_map[embedding.filename] = embedding + end + end + + prompt = prompt:gsub('#' .. prompt_context .. '%s*', '') + end + end + + -- Sort and parse contexts + local contexts = {} + if config.context then + if type(config.context) == 'table' then + for _, config_context in ipairs(config.context) do + table.insert(contexts, config_context) + end + else + table.insert(contexts, config.context) + end + end + for prompt_context in prompt:gmatch('#([^%s]+)') do + table.insert(contexts, prompt_context) + end + table.sort(contexts, function(a, b) + return #a > #b + end) + for _, prompt_context in ipairs(contexts) do + parse_context(prompt_context) + end + + return vim.tbl_values(embedding_map), prompt +end + ---@param config CopilotChat.config ---@param message string? ---@param hide_help boolean? @@ -639,58 +685,19 @@ function M.ask(prompt, config) end -- Resolve prompt references - local system_prompt, updated_prompt = update_prompts(prompt, config.system_prompt) + local system_prompt, resolved_prompt = resolve_prompts(prompt, config.system_prompt) -- Remove sticky prefix prompt = table.concat( vim.tbl_map(function(l) return l:gsub('>%s+', '') - end, vim.split(updated_prompt, '\n')), + end, vim.split(resolved_prompt, '\n')), '\n' ) - local embedding_map = {} - local function parse_context(prompt_context) - local split = vim.split(prompt_context, ':') - local context_name = table.remove(split, 1) - local context_input = vim.trim(table.concat(split, ':')) - local context_value = config.contexts[context_name] - if context_input == '' then - context_input = nil - end - - if context_value then - for _, embedding in ipairs(context_value.resolve(context_input, state.source)) do - if embedding then - embedding_map[embedding.filename] = embedding - end - end - - prompt = prompt:gsub('#' .. prompt_context .. '%s*', '') - end - end - - -- Sort and parse contexts - local contexts = {} - if config.context then - if type(config.context) == 'table' then - for _, config_context in ipairs(config.context) do - table.insert(contexts, config_context) - end - else - table.insert(contexts, config.context) - end - end - for prompt_context in prompt:gmatch('#([^%s]+)') do - table.insert(contexts, prompt_context) - end - table.sort(contexts, function(a, b) - return #a > #b - end) - for _, prompt_context in ipairs(contexts) do - parse_context(prompt_context) - end - local embeddings = vim.tbl_values(embedding_map) + -- Resolve embeddings + local embeddings, embedded_prompt = resolve_embeddings(prompt, config) + prompt = embedded_prompt -- Retrieve the selection local selection = get_selection(config) @@ -954,30 +961,12 @@ function M.setup(config) end) end) - if state.system_prompt then - state.system_prompt:delete() - end - state.system_prompt = Overlay('copilot-system-prompt', overlay_help, function(bufnr) - map_key(M.config.mappings.close, bufnr, function() - state.system_prompt:restore(state.chat.winnr, state.chat.bufnr) - end) - end) - - if state.user_selection then - state.user_selection:delete() + if state.overlay then + state.overlay:delete() end - state.user_selection = Overlay('copilot-user-selection', overlay_help, function(bufnr) + state.overlay = Overlay('copilot-overlay', overlay_help, function(bufnr) map_key(M.config.mappings.close, bufnr, function() - state.user_selection:restore(state.chat.winnr, state.chat.bufnr) - end) - end) - - if state.help then - state.help:delete() - end - state.help = Overlay('copilot-help', overlay_help, function(bufnr) - map_key(M.config.mappings.close, bufnr, function() - state.help:restore(state.chat.winnr, state.chat.bufnr) + state.overlay:restore(state.chat.winnr, state.chat.bufnr) end) end) @@ -1015,7 +1004,7 @@ function M.setup(config) end end end - state.help:show(chat_help, 'markdown', state.chat.winnr) + state.overlay:show(chat_help, 'markdown', state.chat.winnr) end) map_key(M.config.mappings.reset, bufnr, M.reset) @@ -1167,12 +1156,16 @@ function M.setup(config) end) map_key(M.config.mappings.show_system_prompt, bufnr, function() - local prompt = state.config.system_prompt - if not prompt then + local section = state.chat:get_closest_section() + local system_prompt = state.config.system_prompt + if section and not section.answer then + system_prompt = resolve_prompts(section.content, state.config.system_prompt) + end + if not system_prompt then return end - state.system_prompt:show(vim.trim(prompt) .. '\n', 'markdown', state.chat.winnr) + state.overlay:show(vim.trim(system_prompt) .. '\n', 'markdown', state.chat.winnr) end) map_key(M.config.mappings.show_user_selection, bufnr, function() @@ -1181,7 +1174,30 @@ function M.setup(config) return end - state.user_selection:show(selection.content .. '\n', selection.filetype, state.chat.winnr) + state.overlay:show(selection.content, selection.filetype, state.chat.winnr) + end) + + map_key(M.config.mappings.show_user_context, bufnr, function() + local section = state.chat:get_closest_section() + local embeddings = {} + if section and not section.answer then + embeddings = resolve_embeddings(section.content, state.config) + end + + local text = '' + for _, embedding in ipairs(embeddings) do + local lines = vim.split(embedding.content, '\n') + local preview = table.concat(vim.list_slice(lines, 1, math.min(10, #lines)), '\n') + local header = string.format('**`%s`** (%s lines)', embedding.filename, #lines) + if #lines > 10 then + header = header .. ' (truncated)' + end + + text = text + .. string.format('%s\n```%s\n%s\n```\n\n', header, embedding.filetype, preview) + end + + state.overlay:show(vim.trim(text) .. '\n', 'markdown', state.chat.winnr) end) vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index b3a7dfcc..a28ff8e9 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -42,6 +42,10 @@ function Overlay:validate() end function Overlay:show(text, filetype, winnr, syntax) + if not text or vim.trim(text) == '' then + return + end + self:validate() text = text .. '\n' From f53ae2f17b4321bc4ff90ad9562fb1deeac5794c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 Nov 2024 15:08:14 +0000 Subject: [PATCH 237/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 36aaa25c..8459974d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -175,6 +175,7 @@ CHAT MAPPINGS ~ - `gd` - Show diff between source and nearest diff - `gp` - Show system prompt for current chat - `gs` - Show current user selection +- `gc` - Show current user context - `gh` - Show help message The mappings can be customized by setting the `mappings` table in your @@ -609,6 +610,12 @@ Also see here : show_user_selection = { normal = 'gs' }, + show_user_context = { + normal = 'gc', + }, + show_help = { + normal = 'gh', + }, }, } < From 5e2dd6dee0ac6606c379a0a9aa3ab7ad0650aa35 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 17:11:42 +0100 Subject: [PATCH 238/338] fix: use cwd when getting git diffs and files The current implementation incorrectly used buffer-based paths for git diffs and file listing. This change properly uses the current working directory (cwd) for both operations, ensuring correct paths are used when accessing files and git diffs across the workspace. The cwd field is now added to the source configuration and properly passed through to relevant functions. This fixes issues with file listing and git diff operations when working with files outside the current buffer's directory. Closes #577 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 13 +++++++------ lua/CopilotChat/context.lua | 24 +++++++----------------- lua/CopilotChat/init.lua | 9 ++++++++- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index b59b20d5..bbe14634 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -6,6 +6,7 @@ local utils = require('CopilotChat.utils') --- @class CopilotChat.config.source --- @field bufnr number --- @field winnr number +--- @field cwd string ---@class CopilotChat.config.selection.diagnostic ---@field content string @@ -24,7 +25,7 @@ local utils = require('CopilotChat.utils') ---@class CopilotChat.config.context ---@field description string? ----@field input fun(callback: fun(input: string?))? +---@field input fun(callback: fun(input: string?), source: CopilotChat.config.source)? ---@field resolve fun(input: string?, source: CopilotChat.config.source):table ---@class CopilotChat.config.prompt @@ -188,10 +189,10 @@ return { }, file = { description = 'Includes content of provided file in chat context. Supports input.', - input = function(callback) + input = function(callback, source) local files = vim.tbl_filter(function(file) return vim.fn.isdirectory(file) == 0 - end, vim.fn.glob('**/*', false, true)) + end, vim.fn.glob(source.cwd .. '/**/*', false, true)) vim.ui.select(files, { prompt = 'Select a file> ', @@ -211,8 +212,8 @@ return { default = '**/*', }, callback) end, - resolve = function(input) - return context.files(input) + resolve = function(input, source) + return context.files(input, source.cwd) end, }, git = { @@ -224,7 +225,7 @@ return { end, resolve = function(input, source) return { - context.gitdiff(input, source.bufnr), + context.gitdiff(input, source.cwd), } end, }, diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index d77bc40a..89a8248a 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -191,11 +191,13 @@ end --- Get list of all files in workspace ---@param pattern string? +---@param cwd string ---@return table -function M.files(pattern) +function M.files(pattern, cwd) + local search = cwd .. '/' .. (pattern or '**/*') local files = vim.tbl_filter(function(file) return vim.fn.isdirectory(file) == 0 - end, vim.fn.glob(pattern or '**/*', false, true)) + end, vim.fn.glob(search, false, true)) if #files == 0 then return {} @@ -263,23 +265,11 @@ end --- Get current git diff ---@param type string? ----@param bufnr number +---@param cwd string ---@return CopilotChat.copilot.embed? -function M.gitdiff(type, bufnr) - if not utils.buf_valid(bufnr) then - return nil - end - +function M.gitdiff(type, cwd) type = type or 'unstaged' - local bufname = vim.api.nvim_buf_get_name(bufnr) - local file_path = bufname:gsub('^%w+://', '') - local dir = vim.fn.fnamemodify(file_path, ':h') - if not dir or dir == '' then - return nil - end - dir = dir:gsub('.git$', '') - - local cmd = 'git -C ' .. dir .. ' diff --no-color --no-ext-diff' + local cmd = 'git -C ' .. cwd .. ' diff --no-color --no-ext-diff' if type == 'staged' then cmd = cmd .. ' --staged' diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 45e2fe1b..93413d61 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -92,9 +92,16 @@ end local function update_selection() local prev_winnr = vim.fn.win_getid(vim.fn.winnr('#')) if prev_winnr ~= state.chat.winnr and vim.fn.win_gettype(prev_winnr) == '' then + -- TODO: This is a hack to get the cwd of the previous window properly, its actually baffling I have to do this + local current_win = vim.api.nvim_get_current_win() + vim.api.nvim_set_current_win(prev_winnr) + local cwd = vim.fn.getcwd() + vim.api.nvim_set_current_win(current_win) + state.source = { bufnr = vim.api.nvim_win_get_buf(prev_winnr), winnr = prev_winnr, + cwd = cwd, } end @@ -431,7 +438,7 @@ local function trigger_complete() local value_str = tostring(value) vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { value_str }) vim.api.nvim_win_set_cursor(0, { row, col + #value_str }) - end) + end, state.source) end return From fc1ceceb53b7ff5a7b5c24c52e56ff7a5f46efea Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 17:44:50 +0100 Subject: [PATCH 239/338] fix: get cwd in contexts directly Storing it does not work in the end so have to do it like this. Signed-off-by: Tomas Slusny --- lua/CopilotChat/config.lua | 8 ++++---- lua/CopilotChat/context.lua | 10 ++++++---- lua/CopilotChat/init.lua | 7 ------- lua/CopilotChat/utils.lua | 21 +++++++++++++++++++++ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index bbe14634..2605ed33 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -6,7 +6,6 @@ local utils = require('CopilotChat.utils') --- @class CopilotChat.config.source --- @field bufnr number --- @field winnr number ---- @field cwd string ---@class CopilotChat.config.selection.diagnostic ---@field content string @@ -190,9 +189,10 @@ return { file = { description = 'Includes content of provided file in chat context. Supports input.', input = function(callback, source) + local cwd = utils.win_cwd(source.winnr) local files = vim.tbl_filter(function(file) return vim.fn.isdirectory(file) == 0 - end, vim.fn.glob(source.cwd .. '/**/*', false, true)) + end, vim.fn.glob(cwd .. '/**/*', false, true)) vim.ui.select(files, { prompt = 'Select a file> ', @@ -213,7 +213,7 @@ return { }, callback) end, resolve = function(input, source) - return context.files(input, source.cwd) + return context.files(input, source.winnr) end, }, git = { @@ -225,7 +225,7 @@ return { end, resolve = function(input, source) return { - context.gitdiff(input, source.cwd), + context.gitdiff(input, source.winnr), } end, }, diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 89a8248a..aa53cb83 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -191,9 +191,10 @@ end --- Get list of all files in workspace ---@param pattern string? ----@param cwd string +---@param winnr number ---@return table -function M.files(pattern, cwd) +function M.files(pattern, winnr) + local cwd = utils.win_cwd(winnr) local search = cwd .. '/' .. (pattern or '**/*') local files = vim.tbl_filter(function(file) return vim.fn.isdirectory(file) == 0 @@ -265,10 +266,11 @@ end --- Get current git diff ---@param type string? ----@param cwd string +---@param winnr number ---@return CopilotChat.copilot.embed? -function M.gitdiff(type, cwd) +function M.gitdiff(type, winnr) type = type or 'unstaged' + local cwd = utils.win_cwd(winnr) local cmd = 'git -C ' .. cwd .. ' diff --no-color --no-ext-diff' if type == 'staged' then diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 93413d61..13ed044f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -92,16 +92,9 @@ end local function update_selection() local prev_winnr = vim.fn.win_getid(vim.fn.winnr('#')) if prev_winnr ~= state.chat.winnr and vim.fn.win_gettype(prev_winnr) == '' then - -- TODO: This is a hack to get the cwd of the previous window properly, its actually baffling I have to do this - local current_win = vim.api.nvim_get_current_win() - vim.api.nvim_set_current_win(prev_winnr) - local cwd = vim.fn.getcwd() - vim.api.nvim_set_current_win(current_win) - state.source = { bufnr = vim.api.nvim_win_get_buf(prev_winnr), winnr = prev_winnr, - cwd = cwd, } end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 02bdb3b1..5a9023c5 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -168,4 +168,25 @@ function M.quick_hash(str) return #str .. str:sub(1, 32) .. str:sub(-32) end +--- Get current working directory for target window +--- TODO: This is a hack to get the cwd of the previous window properly, its actually baffling I have to do this +---@param winnr number The buffer number +---@return string +function M.win_cwd(winnr) + if not winnr or not vim.api.nvim_win_is_valid(winnr) then + return '.' + end + + local current_win = vim.api.nvim_get_current_win() + vim.api.nvim_set_current_win(winnr) + local dir = vim.fn.getcwd() + vim.api.nvim_set_current_win(current_win) + + if not dir or dir == '' then + return '.' + end + + return dir +end + return M From e4e6993aaba3c203e489ab2bb236fe5abd9baa94 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 18:13:51 +0100 Subject: [PATCH 240/338] feat: add buffer validation for chat window Add validate() method to Chat class that ensures the window buffer stays in sync. This prevents potential issues where the window could show incorrect buffer content by validating and resetting the buffer if needed. --- lua/CopilotChat/chat.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index ccc10c7f..cd127cb3 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -97,6 +97,17 @@ function Chat:create() return bufnr end +function Chat:validate() + Overlay.validate(self) + if + self.winnr + and vim.api.nvim_win_is_valid(self.winnr) + and vim.api.nvim_win_get_buf(self.winnr) ~= self.bufnr + then + vim.api.nvim_win_set_buf(self.winnr, self.bufnr) + end +end + function Chat:visible() return self.winnr and vim.api.nvim_win_is_valid(self.winnr) From 240ad9167feea71ddd88bfcf0b076de8f81f7f03 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 20:35:57 +0100 Subject: [PATCH 241/338] refactor: improve variable naming and pattern matching - Rename local variables to use SCREAMING_SNAKE_CASE for constants - Simplify pattern matching using WORD constant for consistent regex - Optimize context/embeddings handling to avoid duplicate entries - Make file map chunk naming more consistent - Refactor headers extension in authenticate method --- lua/CopilotChat/context.lua | 29 ++++++++------- lua/CopilotChat/copilot.lua | 29 +++++++-------- lua/CopilotChat/init.lua | 74 +++++++++++++++++++------------------ 3 files changed, 68 insertions(+), 64 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index aa53cb83..3585d57a 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -3,7 +3,7 @@ local utils = require('CopilotChat.utils') local M = {} -local outline_types = { +local OUTLINE_TYPES = { 'local_function', 'function_item', 'arrow_function', @@ -32,18 +32,18 @@ local outline_types = { 'block_quote', } -local comment_types = { +local COMMENT_TYPES = { 'comment', 'line_comment', 'block_comment', 'doc_comment', } -local ignored_types = { +local IGNORED_TYPES = { 'export_statement', } -local off_side_rule_languages = { +local OFF_SIDE_RULE_LANGUAGES = { 'python', 'coffeescript', 'nim', @@ -52,7 +52,7 @@ local off_side_rule_languages = { 'fsharp', } -local outline_threshold = 600 +local OUTLINE_THRESHOLD = 600 local function spatial_distance_cosine(a, b) local dot_product = 0 @@ -121,10 +121,10 @@ function M.outline(content, name, ft) local function get_outline_lines(node) local type = node:type() local parent = node:parent() - local is_outline = vim.tbl_contains(outline_types, type) - local is_comment = vim.tbl_contains(comment_types, type) - local is_ignored = vim.tbl_contains(ignored_types, type) - or parent and vim.tbl_contains(ignored_types, parent:type()) + local is_outline = vim.tbl_contains(OUTLINE_TYPES, type) + local is_comment = vim.tbl_contains(COMMENT_TYPES, type) + local is_ignored = vim.tbl_contains(IGNORED_TYPES, type) + or parent and vim.tbl_contains(IGNORED_TYPES, parent:type()) local start_row, start_col, end_row, end_col = node:range() local skip_inner = false @@ -165,7 +165,7 @@ function M.outline(content, name, ft) end if is_outline then - if not skip_inner and not vim.tbl_contains(off_side_rule_languages, ft) then + if not skip_inner and not vim.tbl_contains(OFF_SIDE_RULE_LANGUAGES, ft) then local end_line = lines[end_row + 1] local signature_end = vim.trim(end_line:sub(1, end_col)) table.insert(outline_lines, string.rep(' ', depth) .. signature_end) @@ -214,9 +214,12 @@ function M.files(pattern, winnr) table.insert(chunk, files[j]) end + local chunk_number = math.floor(i / chunk_size) + local chunk_name = chunk_number == 0 and 'file_map' or 'file_map' .. tostring(chunk_number) + table.insert(out, { content = table.concat(chunk, '\n'), - filename = 'file_map_' .. tostring(i), + filename = chunk_name, filetype = 'text', }) end @@ -336,8 +339,8 @@ function M.filter_embeddings(copilot, prompt, embeddings) local outline_lines = vim.split(outline.content, '\n') -- If outline is too big, truncate it - if #outline_lines > 0 and #outline_lines > outline_threshold then - outline_lines = vim.list_slice(outline_lines, 1, outline_threshold) + if #outline_lines > 0 and #outline_lines > OUTLINE_THRESHOLD then + outline_lines = vim.list_slice(outline_lines, 1, OUTLINE_THRESHOLD) table.insert(outline_lines, '... (truncated)') end diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 71633291..37f3cf49 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -38,10 +38,10 @@ local class = utils.class local temp_file = utils.temp_file --- Constants -local context_format = '[#file:%s](#file:%s-context)\n' -local big_file_threshold = 2000 -local timeout = 30000 -local version_headers = { +local CONTEXT_FORMAT = '[#file:%s](#file:%s-context)' +local BIG_FILE_THRESHOLD = 2000 +local TIMEOUT = 30000 +local VERSION_HEADERS = { ['editor-version'] = 'Neovim/' .. vim.version().major .. '.' @@ -128,8 +128,8 @@ local function generate_line_numbers(content, start_line) local truncated = false -- If the file is too big, truncate it - if #lines > big_file_threshold then - lines = vim.list_slice(lines, 1, big_file_threshold) + if #lines > BIG_FILE_THRESHOLD then + lines = vim.list_slice(lines, 1, BIG_FILE_THRESHOLD) truncated = true end @@ -201,7 +201,7 @@ local function generate_selection_messages(selection) return { { - context = string.format(context_format, filename, filename), + context = string.format(CONTEXT_FORMAT, filename, filename), content = out, role = 'user', }, @@ -225,7 +225,7 @@ local function generate_embeddings_messages(embeddings) for filename, group in pairs(files) do local filetype = group[1].filetype or 'text' table.insert(out, { - context = string.format(context_format, filename, filename), + context = string.format(CONTEXT_FORMAT, filename, filename), content = string.format( '# FILE:%s CONTEXT\n```%s\n%s\n```', filename:upper(), @@ -344,7 +344,7 @@ local Copilot = class(function(self, proxy, allow_insecure) self.claude_enabled = false self.current_job = nil self.request_args = { - timeout = timeout, + timeout = TIMEOUT, proxy = proxy, insecure = allow_insecure, raw = { @@ -356,7 +356,7 @@ local Copilot = class(function(self, proxy, allow_insecure) '1', -- Maximum time for the request '--max-time', - math.floor(timeout * 2 / 1000), + math.floor(TIMEOUT * 2 / 1000), -- Timeout for initial connection '--connect-timeout', '10', @@ -381,13 +381,10 @@ function Copilot:authenticate() not self.token or (self.token.expires_at and self.token.expires_at <= math.floor(os.time())) then local sessionid = utils.uuid() .. tostring(math.floor(os.time() * 1000)) - local headers = { + local headers = vim.tbl_extend('force', { ['authorization'] = 'token ' .. self.github_token, ['accept'] = 'application/json', - } - for key, value in pairs(version_headers) do - headers[key] = value - end + }, VERSION_HEADERS) local response, err = curl_get( 'https://api.github.com/copilot_internal/v2/token', @@ -418,7 +415,7 @@ function Copilot:authenticate() ['openai-intent'] = 'conversation-panel', ['content-type'] = 'application/json', } - for key, value in pairs(version_headers) do + for key, value in pairs(VERSION_HEADERS) do headers[key] = value end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 13ed044f..46d63874 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -11,7 +11,8 @@ local debuginfo = require('CopilotChat.debuginfo') local utils = require('CopilotChat.utils') local M = {} -local plugin_name = 'CopilotChat.nvim' +local PLUGIN_NAME = 'CopilotChat.nvim' +local WORD = '([^%s]+)%s*' --- @class CopilotChat.state --- @field copilot CopilotChat.Copilot? @@ -196,12 +197,12 @@ end local function resolve_prompts(prompt, system_prompt) local prompts_to_use = M.prompts() local try_again = false - local result = string.gsub(prompt, [[/[%w_]+]], function(match) + local result = string.gsub(prompt, '/' .. WORD, function(match) local found = prompts_to_use[string.sub(match, 2)] if found then if found.kind == 'user' then local out = found.prompt - if out and string.match(out, [[/[%w_]+]]) then + if out and string.match(out, '/' .. WORD) then try_again = true end system_prompt = found.system_prompt or system_prompt @@ -227,6 +228,8 @@ end ---@return table, string local function resolve_embeddings(prompt, config) local embedding_map = {} + local embeddings = {} + local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') local context_name = table.remove(split, 1) @@ -238,37 +241,36 @@ local function resolve_embeddings(prompt, config) if context_value then for _, embedding in ipairs(context_value.resolve(context_input, state.source)) do - if embedding then - embedding_map[embedding.filename] = embedding + if embedding and not embedding_map[embedding.filename] then + embedding_map[embedding.filename] = true + table.insert(embeddings, embedding) end end - prompt = prompt:gsub('#' .. prompt_context .. '%s*', '') + return true end + + return false end - -- Sort and parse contexts - local contexts = {} + prompt = prompt:gsub('#' .. WORD, function(match) + if parse_context(match) then + return '' + end + return match + end) + if config.context then if type(config.context) == 'table' then for _, config_context in ipairs(config.context) do - table.insert(contexts, config_context) + parse_context(config_context) end else - table.insert(contexts, config.context) + parse_context(config.context) end end - for prompt_context in prompt:gmatch('#([^%s]+)') do - table.insert(contexts, prompt_context) - end - table.sort(contexts, function(a, b) - return #a > #b - end) - for _, prompt_context in ipairs(contexts) do - parse_context(prompt_context) - end - return vim.tbl_values(embedding_map), prompt + return embeddings, prompt end ---@param config CopilotChat.config @@ -705,23 +707,25 @@ function M.ask(prompt, config) async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) local selected_agent = config.agent - for agent in prompt:gmatch('@([^%s]+)') do - if vim.tbl_contains(agents, agent) then - selected_agent = agent - prompt = prompt:gsub('@' .. agent .. '%s*', '') + prompt = prompt:gsub('@' .. WORD, function(match) + if vim.tbl_contains(agents, match) then + selected_agent = match + return '' end - end + return match + end) local models = vim.tbl_keys(state.copilot:list_models()) - local has_output = false local selected_model = config.model - for model in prompt:gmatch('%$([^%s]+)') do - if vim.tbl_contains(models, model) then - selected_model = model - prompt = prompt:gsub('%$' .. model .. '%s*', '') + prompt = prompt:gsub('%$' .. WORD, function(match) + if vim.tbl_contains(models, match) then + selected_model = match + return '' end - end + return match + end) + local has_output = false local query_ok, filtered_embeddings = pcall(context.filter_embeddings, state.copilot, prompt, embeddings) @@ -869,9 +873,9 @@ end function M.log_level(level) M.config.log_level = level M.config.debug = level == 'debug' - local logfile = string.format('%s/%s.log', vim.fn.stdpath('state'), plugin_name) + local logfile = string.format('%s/%s.log', vim.fn.stdpath('state'), PLUGIN_NAME) log.new({ - plugin = plugin_name, + plugin = PLUGIN_NAME, level = level, outfile = logfile, }, true) @@ -1257,13 +1261,13 @@ function M.setup(config) nargs = '*', force = true, range = true, - desc = prompt.description or (plugin_name .. ' ' .. name), + desc = prompt.description or (PLUGIN_NAME .. ' ' .. name), }) if prompt.mapping then vim.keymap.set({ 'n', 'v' }, prompt.mapping, function() M.ask(prompt.prompt, prompt) - end, { desc = prompt.description or (plugin_name .. ' ' .. name) }) + end, { desc = prompt.description or (PLUGIN_NAME .. ' ' .. name) }) end end From 3766c00fa9850b385c6a5077d286ec75f2aeabf1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 21:00:47 +0100 Subject: [PATCH 242/338] fix: set default completeopt values for chat buffer Automatically add 'popup' and 'noinsert' to completeopt if they are not present when creating chat buffer. This ensures consistent completion behavior across different Neovim configurations. --- lua/CopilotChat/chat.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index cd127cb3..3c7bb9fd 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -79,6 +79,21 @@ function Chat:create() vim.bo[bufnr].syntax = 'markdown' vim.bo[bufnr].textwidth = 0 + -- Add popup and noinsert if not present + local completeopt = vim.opt.completeopt:get() + local updated = false + if not vim.tbl_contains(completeopt, 'noinsert') then + updated = true + table.insert(completeopt, 'noinsert') + end + if not vim.tbl_contains(completeopt, 'popup') then + updated = true + table.insert(completeopt, 'popup') + end + if updated then + vim.bo[bufnr].completeopt = table.concat(completeopt, ',') + end + vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { buffer = bufnr, callback = function() From 8b1c6d4aa825c98206ce3dda55a2cc6b7cebb913 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 22:55:21 +0100 Subject: [PATCH 243/338] refactor: improve embedding handling with ordered map Replaces ad-hoc embedding handling with a new OrderedMap data structure to maintain insertion order and prevent duplicates. This change improves code organization and performance by: - Adding new OrderedMap utility class for ordered key-value storage - Refactoring embedding collection to use OrderedMap - Optimizing file token budget allocation to process from back to front - Adding threshold for multi-file embeddings processing - Preserving command prefixes in prompt processing The changes make the code more maintainable and efficient when handling multiple file contexts in chat interactions. --- lua/CopilotChat/context.lua | 33 ++++++++++++++++++------------- lua/CopilotChat/copilot.lua | 5 +++-- lua/CopilotChat/init.lua | 20 +++++++++---------- lua/CopilotChat/utils.lua | 39 +++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 3585d57a..b421a97f 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -53,6 +53,7 @@ local OFF_SIDE_RULE_LANGUAGES = { } local OUTLINE_THRESHOLD = 600 +local MULTI_FILE_THRESHOLD = 3 local function spatial_distance_cosine(a, b) local dot_product = 0 @@ -322,18 +323,23 @@ end ---@return table function M.filter_embeddings(copilot, prompt, embeddings) -- If we dont need to embed anything, just return directly - if #embeddings == 0 then + if #embeddings < MULTI_FILE_THRESHOLD then return embeddings end - table.insert(embeddings, 1, { + local original_map = utils.ordered_map() + local embedded_map = utils.ordered_map() + + embedded_map:set('prompt', { content = prompt, filename = 'prompt', filetype = 'raw', }) - -- Get embeddings from outlines - local embedded_data = copilot:embed(vim.tbl_map(function(embed) + -- Map embeddings by filename + for _, embed in ipairs(embeddings) do + original_map:set(embed.filename, embed) + if embed.filetype ~= 'raw' then local outline = M.outline(embed.content, embed.filename, embed.filetype) local outline_lines = vim.split(outline.content, '\n') @@ -345,11 +351,14 @@ function M.filter_embeddings(copilot, prompt, embeddings) end outline.content = table.concat(outline_lines, '\n') - return outline + embedded_map:set(embed.filename, outline) + else + embedded_map:set(embed.filename, embed) end + end - return embed - end, embeddings)) + -- Get embeddings from all items + local embedded_data = copilot:embed(embedded_map:values()) -- Rate embeddings by relatedness to the query local ranked_data = data_ranked_by_relatedness(table.remove(embedded_data, 1), embedded_data, 20) @@ -358,14 +367,12 @@ function M.filter_embeddings(copilot, prompt, embeddings) log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end - -- Return original content but keep the ranking order and scores + -- Return original content in ranked order local result = {} for _, ranked_item in ipairs(ranked_data) do - for _, original in ipairs(embeddings) do - if original.filename == ranked_item.filename then - table.insert(result, original) - break - end + local original = original_map:get(ranked_item.filename) + if original then + table.insert(result, original) end end diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 37f3cf49..53928300 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -597,9 +597,10 @@ function Copilot:ask(prompt, opts) history_tokens = history_tokens - tiktoken.count(removed.content) end - -- Now add as many files as possible with remaining token budget + -- Now add as many files as possible with remaining token budget (back to front) local remaining_tokens = max_tokens - required_tokens - history_tokens - for _, message in ipairs(embeddings_messages) do + for i = #embeddings_messages, 1, -1 do + local message = embeddings_messages[i] local tokens = tiktoken.count(message.content) if remaining_tokens - tokens >= 0 then remaining_tokens = remaining_tokens - tokens diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 46d63874..ea80aef6 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -12,7 +12,7 @@ local utils = require('CopilotChat.utils') local M = {} local PLUGIN_NAME = 'CopilotChat.nvim' -local WORD = '([^%s]+)%s*' +local WORD = '([^%s]+)' --- @class CopilotChat.state --- @field copilot CopilotChat.Copilot? @@ -213,7 +213,7 @@ local function resolve_prompts(prompt, system_prompt) end end - return match + return '/' .. match end) if try_again then @@ -227,8 +227,7 @@ end ---@param config CopilotChat.config ---@return table, string local function resolve_embeddings(prompt, config) - local embedding_map = {} - local embeddings = {} + local embeddings = utils.ordered_map() local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') @@ -241,9 +240,8 @@ local function resolve_embeddings(prompt, config) if context_value then for _, embedding in ipairs(context_value.resolve(context_input, state.source)) do - if embedding and not embedding_map[embedding.filename] then - embedding_map[embedding.filename] = true - table.insert(embeddings, embedding) + if embedding then + embeddings:set(embedding.filename, embedding) end end @@ -257,7 +255,7 @@ local function resolve_embeddings(prompt, config) if parse_context(match) then return '' end - return match + return '#' .. match end) if config.context then @@ -270,7 +268,7 @@ local function resolve_embeddings(prompt, config) end end - return embeddings, prompt + return embeddings:values(), prompt end ---@param config CopilotChat.config @@ -712,7 +710,7 @@ function M.ask(prompt, config) selected_agent = match return '' end - return match + return '@' .. match end) local models = vim.tbl_keys(state.copilot:list_models()) @@ -722,7 +720,7 @@ function M.ask(prompt, config) selected_model = match return '' end - return match + return '$' .. match end) local has_output = false diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 5a9023c5..922fdcd5 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -189,4 +189,43 @@ function M.win_cwd(winnr) return dir end +---@class OrderedMap +---@field set fun(self:OrderedMap, key:any, value:any) +---@field get fun(self:OrderedMap, key:any):any +---@field keys fun(self:OrderedMap):table +---@field values fun(self:OrderedMap):table + +--- Create an ordered map +---@return OrderedMap +function M.ordered_map() + local ordered_map = { + _keys = {}, + _data = {}, + set = function(self, key, value) + if not self._data[key] then + table.insert(self._keys, key) + end + self._data[key] = value + end, + + get = function(self, key) + return self._data[key] + end, + + keys = function(self) + return self._keys + end, + + values = function(self) + local result = {} + for _, key in ipairs(self._keys) do + table.insert(result, self._data[key]) + end + return result + end, + } + + return ordered_map +end + return M From 1335c6f015d1ae1d18c88a4bc2439f44e039b14a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 23:15:40 +0100 Subject: [PATCH 244/338] fix: guard completeopt setting behind nvim-0.11.0 check The completeopt buffer option was being set unconditionally, which could cause errors in older Neovim versions. This change adds a version check to ensure the setting is only modified on Neovim 0.11.0 and newer versions. Signed-off-by: Tomas Slusny --- README.md | 3 +++ lua/CopilotChat/chat.lua | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e82e13f9..cfa249bf 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,9 @@ Optional: > For Arch Linux user, you can install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from aur! +> [!WARNING] +> If you are on neovim < 0.11.0, you also might want to add `noinsert` and `popup` to your `completeopt` to make the chat completion behave well. + ## Installation ### Lazy.nvim diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 3c7bb9fd..781b43de 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -91,7 +91,9 @@ function Chat:create() table.insert(completeopt, 'popup') end if updated then - vim.bo[bufnr].completeopt = table.concat(completeopt, ',') + if vim.fn.has('nvim-0.11.0') == 1 then + vim.bo[bufnr].completeopt = table.concat(completeopt, ',') + end end vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { From 9931c85a96e13a20a2852f597e9f96f4395f8c69 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 21 Nov 2024 22:21:04 +0000 Subject: [PATCH 245/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 8459974d..39ce0101 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -71,6 +71,9 @@ Optional: `lua51-tiktoken-bin` from aur! + [!WARNING] If you are on neovim < 0.11.0, you also might want to add `noinsert` + and `popup` to your `completeopt` to make the chat completion behave well. + INSTALLATION *CopilotChat-installation* From 96875a0a931af211efa6b4080ea91f9217035906 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 23:24:33 +0100 Subject: [PATCH 246/338] fix: set completeopt on window opening and properly guard behind config Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 781b43de..51a35cfc 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -79,23 +79,6 @@ function Chat:create() vim.bo[bufnr].syntax = 'markdown' vim.bo[bufnr].textwidth = 0 - -- Add popup and noinsert if not present - local completeopt = vim.opt.completeopt:get() - local updated = false - if not vim.tbl_contains(completeopt, 'noinsert') then - updated = true - table.insert(completeopt, 'noinsert') - end - if not vim.tbl_contains(completeopt, 'popup') then - updated = true - table.insert(completeopt, 'popup') - end - if updated then - if vim.fn.has('nvim-0.11.0') == 1 then - vim.bo[bufnr].completeopt = table.concat(completeopt, ',') - end - end - vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { buffer = bufnr, callback = function() @@ -424,6 +407,23 @@ function Chat:open(config) self.answer_header = config.answer_header self.separator = config.separator + if config.chat_autocomplete and vim.fn.has('nvim-0.11.0') == 1 then + -- Add popup and noinsert if not present + local completeopt = vim.opt.completeopt:get() + local updated = false + if not vim.tbl_contains(completeopt, 'noinsert') then + updated = true + table.insert(completeopt, 'noinsert') + end + if not vim.tbl_contains(completeopt, 'popup') then + updated = true + table.insert(completeopt, 'popup') + end + if updated then + vim.bo[self.bufnr].completeopt = table.concat(completeopt, ',') + end + end + vim.wo[self.winnr].wrap = true vim.wo[self.winnr].linebreak = true vim.wo[self.winnr].cursorline = true From b6a1119b974dc66bc559221dfc00cfc2a4dd04a1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 23:45:12 +0100 Subject: [PATCH 247/338] fix: improve window current working directory handling Previously, getting window cwd required switching active windows which could cause unwanted side effects. Now, the cwd is stored in window variables and updated via DirChanged autocmd, making the operation more reliable and avoiding window switching. Closes #582 Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 13 +++++++++++++ lua/CopilotChat/utils.lua | 7 +------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index ea80aef6..5f5b8062 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1320,6 +1320,19 @@ function M.setup(config) vim.api.nvim_create_user_command('CopilotChatLoad', function(args) M.load(args.args) end, { nargs = '*', force = true, complete = complete_load }) + + local augroup = vim.api.nvim_create_augroup('CopilotChat', {}) + + -- Store the current directory to window when directory changes + -- I dont think there is a better way to do this that functions + -- with "rooter" plugins, LSP and stuff as vim.fn.getcwd() when + -- i pass window number inside doesnt work + vim.api.nvim_create_autocmd('DirChanged', { + group = augroup, + callback = function() + vim.api.nvim_win_set_var(0, 'cchat_cwd', vim.fn.getcwd()) + end, + }) end return M diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 922fdcd5..b1f2d735 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -169,7 +169,6 @@ function M.quick_hash(str) end --- Get current working directory for target window ---- TODO: This is a hack to get the cwd of the previous window properly, its actually baffling I have to do this ---@param winnr number The buffer number ---@return string function M.win_cwd(winnr) @@ -177,11 +176,7 @@ function M.win_cwd(winnr) return '.' end - local current_win = vim.api.nvim_get_current_win() - vim.api.nvim_set_current_win(winnr) - local dir = vim.fn.getcwd() - vim.api.nvim_set_current_win(current_win) - + local dir = vim.api.nvim_win_get_var(winnr, 'cchat_cwd') if not dir or dir == '' then return '.' end From 7ab495edd427c2173938ecd7044f378559639dc6 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 21 Nov 2024 23:51:29 +0100 Subject: [PATCH 248/338] refactor: use cwd from dir changed event slightly cleaner Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 5f5b8062..0758f897 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1330,7 +1330,9 @@ function M.setup(config) vim.api.nvim_create_autocmd('DirChanged', { group = augroup, callback = function() - vim.api.nvim_win_set_var(0, 'cchat_cwd', vim.fn.getcwd()) + if vim.v.event and vim.v.event.cwd then + vim.api.nvim_win_set_var(0, 'cchat_cwd', vim.v.event.cwd) + end end, }) end From 2c2256bd420d2f185e60598828af9adec55a47af Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 22 Nov 2024 00:41:43 +0100 Subject: [PATCH 249/338] fix: improve chat prompt clearing and cwd detection Clear chat prompt logic has been moved to dedicated method in Chat class to improve code reusability and maintainability. Window local cwd detection has been simplified and now uses VimEnter/WinEnter events instead of DirChanged for more reliable current working directory tracking. Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 18 ++++++++++++++++++ lua/CopilotChat/init.lua | 19 +++---------------- lua/CopilotChat/utils.lua | 4 ++-- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 51a35cfc..d2eb5001 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -4,6 +4,7 @@ ---@field sections table ---@field get_closest_section fun(self: CopilotChat.Chat): table|nil ---@field get_closest_block fun(self: CopilotChat.Chat): table|nil +---@field clear_prompt fun(self: CopilotChat.Chat) ---@field valid fun(self: CopilotChat.Chat) ---@field visible fun(self: CopilotChat.Chat) ---@field active fun(self: CopilotChat.Chat) @@ -278,6 +279,23 @@ function Chat:get_closest_block() } end +function Chat:clear_prompt() + if not self:visible() then + return + end + + self:render() + + local section = self.sections[#self.sections] + if not section or section.answer then + return + end + + vim.bo[self.bufnr].modifiable = true + vim.api.nvim_buf_set_lines(self.bufnr, section.start_line - 1, section.end_line, false, {}) + vim.bo[self.bufnr].modifiable = false +end + function Chat:active() return vim.api.nvim_get_current_win() == self.winnr end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0758f897..fa2a39ac 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -667,19 +667,8 @@ function M.ask(prompt, config) finish(config, nil, true) end - local section = state.chat:get_closest_section() - if not section or section.answer then - return - end - state.last_prompt = prompt - vim.api.nvim_buf_set_lines( - state.chat.bufnr, - section.start_line - 1, - section.end_line, - false, - {} - ) + state.chat:clear_prompt() state.chat:append('\n\n' .. prompt) state.chat:append('\n\n' .. config.answer_header .. config.separator .. '\n\n') end @@ -1327,12 +1316,10 @@ function M.setup(config) -- I dont think there is a better way to do this that functions -- with "rooter" plugins, LSP and stuff as vim.fn.getcwd() when -- i pass window number inside doesnt work - vim.api.nvim_create_autocmd('DirChanged', { + vim.api.nvim_create_autocmd({ 'VimEnter', 'WinEnter' }, { group = augroup, callback = function() - if vim.v.event and vim.v.event.cwd then - vim.api.nvim_win_set_var(0, 'cchat_cwd', vim.v.event.cwd) - end + vim.api.nvim_win_set_var(0, 'cchat_cwd', vim.fn.getcwd()) end, }) end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index b1f2d735..510013a9 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -176,8 +176,8 @@ function M.win_cwd(winnr) return '.' end - local dir = vim.api.nvim_win_get_var(winnr, 'cchat_cwd') - if not dir or dir == '' then + local ok, dir = pcall(vim.api.nvim_win_get_var, winnr, 'cchat_cwd') + if not ok or not dir or dir == '' then return '.' end From a16c0702778d0ce11a41abaedded43e518539764 Mon Sep 17 00:00:00 2001 From: Tomas Janousek Date: Thu, 21 Nov 2024 23:41:01 +0000 Subject: [PATCH 250/338] refactor: simplify cwd detection Turns out nvim_win_get_var was a drama queen, throwing exceptions whenever it couldn't find its precious variable - like a toddler having a meltdown when they can't find their favorite toy. Switched to vim.w, the more chill cousin who just returns nil and goes about their day when something's missing. Now our code won't have an existential crisis just because cchat_cwd wasn't set for one window. No more exception-al behavior, just peaceful, quiet coding. --- lua/CopilotChat/init.lua | 2 +- lua/CopilotChat/utils.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index fa2a39ac..39ae19cb 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1319,7 +1319,7 @@ function M.setup(config) vim.api.nvim_create_autocmd({ 'VimEnter', 'WinEnter' }, { group = augroup, callback = function() - vim.api.nvim_win_set_var(0, 'cchat_cwd', vim.fn.getcwd()) + vim.w.cchat_cwd = vim.fn.getcwd() end, }) end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 510013a9..db22887b 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -176,8 +176,8 @@ function M.win_cwd(winnr) return '.' end - local ok, dir = pcall(vim.api.nvim_win_get_var, winnr, 'cchat_cwd') - if not ok or not dir or dir == '' then + local dir = vim.w[winnr].cchat_cwd + if not dir or dir == '' then return '.' end From fa52213842a6d7c4faf8e056c15346f9ec5e3e46 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 22 Nov 2024 00:15:02 +0000 Subject: [PATCH 251/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 39ce0101..1ea381b7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 21 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 22 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From d480e399c4e3221b16aa66fcf913e6c57eaa46b7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 22 Nov 2024 01:19:33 +0100 Subject: [PATCH 252/338] fix: also use dirchanged for tracking win dir on top of winenter Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 39ae19cb..660d91fa 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1316,7 +1316,7 @@ function M.setup(config) -- I dont think there is a better way to do this that functions -- with "rooter" plugins, LSP and stuff as vim.fn.getcwd() when -- i pass window number inside doesnt work - vim.api.nvim_create_autocmd({ 'VimEnter', 'WinEnter' }, { + vim.api.nvim_create_autocmd({ 'VimEnter', 'WinEnter', 'DirChanged' }, { group = augroup, callback = function() vim.w.cchat_cwd = vim.fn.getcwd() From 197efc320fe73e63490573eed1fe53195d9c20dd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 22 Nov 2024 08:48:02 +0100 Subject: [PATCH 253/338] perf: optimize chat section detection Replace pattern matching with direct string comparison when detecting section headers in chat rendering. This change improves performance by avoiding unnecessary regex operations for simple string equality checks. --- lua/CopilotChat/chat.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index d2eb5001..8f8d4261 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -127,7 +127,7 @@ function Chat:render() for l, line in ipairs(lines) do local separator_found = false - if line:match(self.answer_header .. self.separator .. '$') then + if line == self.answer_header .. self.separator then separator_found = true if current_section then current_section.end_line = l - 1 @@ -138,7 +138,7 @@ function Chat:render() start_line = l + 1, blocks = {}, } - elseif line:match(self.question_header .. self.separator .. '$') then + elseif line == self.question_header .. self.separator then separator_found = true if current_section then current_section.end_line = l - 1 From d8ffcfe1ebc9a9bd3b89db809ecd2830a7406053 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 22 Nov 2024 10:06:21 +0100 Subject: [PATCH 254/338] feat: improve help display and token count visibility The help display and token count mechanism has been improved by: - Moving token count display to be part of the help message - Adding clear help functionality to overlay - Cleaning up finish function parameters and logic - Using dedicated namespace for spinner - Simplifying help display logic in chat rendering This change makes the UI cleaner and more consistent by integrating the token count into the help display system instead of showing it separately. --- lua/CopilotChat/chat.lua | 33 +++++++++++++++++-------------- lua/CopilotChat/init.lua | 39 ++++++++++++------------------------- lua/CopilotChat/overlay.lua | 5 +++++ lua/CopilotChat/spinner.lua | 2 +- 4 files changed, 36 insertions(+), 43 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 8f8d4261..7f739a74 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -65,6 +65,10 @@ local Chat = class(function(self, help, on_buf_create) self.spinner = nil self.sections = {} + -- Variables + self.token_count = nil + self.token_max_count = nil + -- Config self.layout = nil self.auto_insert = false @@ -202,6 +206,17 @@ function Chat:render() end end + local last_section = sections[#sections] + if last_section and not last_section.answer then + local msg = self.help + if self.token_count and self.token_max_count then + msg = msg .. '\n' .. self.token_count .. '/' .. self.token_max_count .. ' tokens used' + end + self:show_help(msg, last_section.start_line - last_section.end_line - 1) + else + self:clear_help() + end + self.sections = sections end @@ -355,6 +370,8 @@ end function Chat:clear() self:validate() + self.token_count = nil + self.token_max_count = nil vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) vim.bo[self.bufnr].modifiable = false @@ -498,27 +515,13 @@ function Chat:follow() vim.api.nvim_win_set_cursor(self.winnr, { last_line + 1, last_column }) end -function Chat:finish(msg, offset) +function Chat:finish() if not self.spinner then return end - if not offset then - offset = 0 - end - self.spinner:finish() - - if msg and msg ~= '' then - if self.help and self.help ~= '' then - msg = msg .. '\n' .. self.help - end - else - msg = self.help - end - vim.bo[self.bufnr].modifiable = true - self:show_help(msg, -offset) if self.auto_insert and self:active() then vim.cmd('startinsert') end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 660d91fa..69c753e2 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -272,10 +272,8 @@ local function resolve_embeddings(prompt, config) end ---@param config CopilotChat.config ----@param message string? ----@param hide_help boolean? ---@param start_of_chat boolean? -local function finish(config, message, hide_help, start_of_chat) +local function finish(config, start_of_chat) if config.no_chat then return end @@ -286,28 +284,18 @@ local function finish(config, message, hide_help, start_of_chat) state.chat:append(config.question_header .. config.separator .. '\n\n') - local offset = 0 - if state.last_prompt then + local has_sticky = false for sticky_line in state.last_prompt:gmatch('(>%s+[^\n]+)') do state.chat:append(sticky_line .. '\n') - -- Account for sticky line - offset = offset + 1 + has_sticky = true end - - if offset > 0 then + if has_sticky then state.chat:append('\n') - -- Account for new line after sticky lines - offset = offset + 1 end end - -- Account for double new line after separator - offset = offset + 2 - - if not hide_help then - state.chat:finish(message, offset) - end + state.chat:finish() end ---@param config CopilotChat.config @@ -664,7 +652,7 @@ function M.ask(prompt, config) if config.clear_chat_on_new_prompt then M.stop(true, config) elseif state.copilot:stop() then - finish(config, nil, true) + finish(config) end state.last_prompt = prompt @@ -757,15 +745,12 @@ function M.ask(prompt, config) if not config.no_chat then state.last_response = response + state.chat.token_count = token_count + state.chat.token_max_count = token_max_count end vim.schedule(function() - if token_count and token_max_count and token_count > 0 then - finish(config, token_count .. '/' .. token_max_count .. ' tokens used') - else - finish(config) - end - + finish(config) if config.callback then config.callback(response, state.source) end @@ -793,7 +778,7 @@ function M.stop(reset, config) state.last_response = nil end - finish(config, nil, nil, reset) + finish(config, reset) end) end @@ -851,7 +836,7 @@ function M.load(name, history_path) end end - finish(M.config, nil, nil, #history == 0) + finish(M.config, #history == 0) M.open() end @@ -1231,7 +1216,7 @@ function M.setup(config) }) end - finish(M.config, nil, nil, true) + finish(M.config, true) end ) diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/overlay.lua index a28ff8e9..10808e22 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/overlay.lua @@ -8,6 +8,7 @@ ---@field restore fun(self: CopilotChat.Overlay, winnr: number, bufnr: number) ---@field delete fun(self: CopilotChat.Overlay) ---@field show_help fun(self: CopilotChat.Overlay, msg: string, offset: number) +---@field clear_help fun(self: CopilotChat.Overlay) local utils = require('CopilotChat.utils') local class = utils.class @@ -101,4 +102,8 @@ function Overlay:show_help(msg, offset) }) end +function Overlay:clear_help() + vim.api.nvim_buf_del_extmark(self.bufnr, self.help_ns, 1) +end + return Overlay diff --git a/lua/CopilotChat/spinner.lua b/lua/CopilotChat/spinner.lua index 1d365c43..b252a986 100644 --- a/lua/CopilotChat/spinner.lua +++ b/lua/CopilotChat/spinner.lua @@ -21,7 +21,7 @@ local spinner_frames = { } local Spinner = class(function(self, bufnr) - self.ns = vim.api.nvim_create_namespace('copilot-chat-help') + self.ns = vim.api.nvim_create_namespace('copilot-chat-spinner') self.bufnr = bufnr self.timer = nil self.index = 1 From 49d3440831df1ec03d33a3aa242f543993fdcf9e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 22 Nov 2024 13:28:06 +0100 Subject: [PATCH 255/338] fix: improve chat window handling and auto-insert behavior - Fix loading of auto-insert-mode config - Fix auto-insert mode to only trigger when buffer is modifiable - Remove unnecessary undojoin command in chat append - Simplify stop/reset logic and remove scheduling wrapper - Fix return to normal mode function to always call stopinsert --- lua/CopilotChat/chat.lua | 14 ++++++++------ lua/CopilotChat/init.lua | 28 ++++++++++------------------ lua/CopilotChat/utils.lua | 3 +-- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 7f739a74..2457c34e 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -435,7 +435,7 @@ function Chat:open(config) end self.layout = layout - self.auto_insert = config.auto_insert + self.auto_insert = config.auto_insert_mode self.auto_follow_cursor = config.auto_follow_cursor self.highlight_headers = config.highlight_headers self.question_header = config.question_header @@ -494,11 +494,13 @@ function Chat:close(bufnr) end function Chat:focus() - if self:visible() then - vim.api.nvim_set_current_win(self.winnr) - if self.auto_insert and self:active() then - vim.cmd('startinsert') - end + if not self:visible() then + return + end + + vim.api.nvim_set_current_win(self.winnr) + if self.auto_insert and self:active() and vim.bo[self.bufnr].modifiable then + vim.cmd('startinsert') end end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 69c753e2..a6142814 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -652,7 +652,7 @@ function M.ask(prompt, config) if config.clear_chat_on_new_prompt then M.stop(true, config) elseif state.copilot:stop() then - finish(config) + finish(config, nil) end state.last_prompt = prompt @@ -723,7 +723,6 @@ function M.ask(prompt, config) on_progress = function(token) vim.schedule(function() if not config.no_chat then - vim.cmd('undojoin') state.chat:append(token) end @@ -763,23 +762,17 @@ end ---@param config CopilotChat.config? function M.stop(reset, config) config = vim.tbl_deep_extend('force', M.config, config or {}) - local stopped = reset and state.copilot:reset() or state.copilot:stop() - local wrap = vim.schedule - if not stopped then - wrap = function(fn) - fn() - end - end - wrap(function() - if reset then - state.chat:clear() - state.last_prompt = nil - state.last_response = nil - end + if reset then + state.copilot:reset() + state.chat:clear() + state.last_prompt = nil + state.last_response = nil + else + state.copilot:stop() + end - finish(config, reset) - end) + finish(config, reset) end --- Reset the chat window and show the help message. @@ -895,7 +888,6 @@ function M.setup(config) if state.copilot then state.copilot:stop() end - state.copilot = Copilot(M.config.proxy, M.config.allow_insecure) if M.config.debug then diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index db22887b..10698e77 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -99,9 +99,8 @@ function M.return_to_normal_mode() local mode = vim.fn.mode():lower() if mode:find('v') then vim.cmd([[execute "normal! \"]]) - elseif mode:find('i') then - vim.cmd('stopinsert') end + vim.cmd('stopinsert') end --- Mark a function as deprecated From 6b24e85210008569fcbc889c525cbc7828deafc2 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 22 Nov 2024 14:08:24 +0100 Subject: [PATCH 256/338] fix: properly parse prompts Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index a6142814..28d217a1 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -198,17 +198,17 @@ local function resolve_prompts(prompt, system_prompt) local prompts_to_use = M.prompts() local try_again = false local result = string.gsub(prompt, '/' .. WORD, function(match) - local found = prompts_to_use[string.sub(match, 2)] - if found then - if found.kind == 'user' then - local out = found.prompt + local matched_prompt = prompts_to_use[match] + if matched_prompt then + if matched_prompt.kind == 'user' then + local out = matched_prompt.prompt if out and string.match(out, '/' .. WORD) then try_again = true end - system_prompt = found.system_prompt or system_prompt + system_prompt = matched_prompt.system_prompt or system_prompt return out - elseif found.kind == 'system' then - system_prompt = found.prompt + elseif matched_prompt.kind == 'system' then + system_prompt = matched_prompt.prompt return '' end end From 5a935ffbdf3d790ae7b51fee358ce7252365b5e9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 22 Nov 2024 23:41:43 +0100 Subject: [PATCH 257/338] refactor(types): improve type safety and organization - Add proper type annotations and documentation for all classes and methods - Reorganize UI components into separate ui/ directory - Remove redundant type comments in favor of proper annotations - Improve class hierarchy and inheritance relationships - Clean up function signatures and return types - Fix overlay component argument order consistency Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 2 +- lua/CopilotChat/copilot.lua | 53 ++++++++----- lua/CopilotChat/init.lua | 58 +++++++------- lua/CopilotChat/{ => ui}/chat.lua | 76 +++++++++++++------ .../{debuginfo.lua => ui/debug.lua} | 57 +++++++++----- lua/CopilotChat/{ => ui}/diff.lua | 27 +++---- lua/CopilotChat/{ => ui}/overlay.lua | 34 +++++---- lua/CopilotChat/{ => ui}/spinner.lua | 11 ++- lua/CopilotChat/utils.lua | 12 +-- 9 files changed, 198 insertions(+), 132 deletions(-) rename lua/CopilotChat/{ => ui}/chat.lua (89%) rename lua/CopilotChat/{debuginfo.lua => ui/debug.lua} (62%) rename lua/CopilotChat/{ => ui}/diff.lua (78%) rename lua/CopilotChat/{ => ui}/overlay.lua (78%) rename lua/CopilotChat/{ => ui}/spinner.lua (86%) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index b421a97f..63adfac5 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -192,7 +192,7 @@ end --- Get list of all files in workspace ---@param pattern string? ----@param winnr number +---@param winnr number? ---@return table function M.files(pattern, winnr) local cwd = utils.win_cwd(winnr) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 53928300..f7b703d7 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -17,17 +17,6 @@ ---@field model string? ---@field chunk_size number? ----@class CopilotChat.Copilot ----@field ask fun(self: CopilotChat.Copilot, prompt: string, opts: CopilotChat.copilot.ask.opts):string,number,number ----@field embed fun(self: CopilotChat.Copilot, inputs: table, opts: CopilotChat.copilot.embed.opts?):table ----@field stop fun(self: CopilotChat.Copilot):boolean ----@field reset fun(self: CopilotChat.Copilot):boolean ----@field save fun(self: CopilotChat.Copilot, name: string, path: string):nil ----@field load fun(self: CopilotChat.Copilot, name: string, path: string):table ----@field running fun(self: CopilotChat.Copilot):boolean ----@field list_models fun(self: CopilotChat.Copilot):table ----@field list_agents fun(self: CopilotChat.Copilot):table - local async = require('plenary.async') local log = require('plenary.log') local curl = require('plenary.curl') @@ -331,18 +320,31 @@ local function generate_embedding_request(inputs, model) } end +---@class CopilotChat.Copilot : CopilotChat.utils.Class +---@field history table +---@field embedding_cache table +---@field policies table +---@field models table? +---@field agents table? +---@field current_job string? +---@field github_token string? +---@field token table? +---@field sessionid string? +---@field machineid string +---@field request_args table local Copilot = class(function(self, proxy, allow_insecure) self.history = {} self.embedding_cache = {} self.policies = {} + self.models = nil + self.agents = nil + + self.current_job = nil self.github_token = nil self.token = nil self.sessionid = nil self.machineid = utils.machine_id() - self.models = nil - self.agents = nil - self.claude_enabled = false - self.current_job = nil + self.request_args = { timeout = TIMEOUT, proxy = proxy, @@ -367,6 +369,8 @@ local Copilot = class(function(self, proxy, allow_insecure) } end) +--- Authenticate with GitHub and get the required headers +---@return table function Copilot:authenticate() if not self.github_token then self.github_token = get_cached_token() @@ -422,6 +426,8 @@ function Copilot:authenticate() return headers end +--- Fetch models from the Copilot API +---@return table function Copilot:fetch_models() if self.models then return self.models @@ -461,6 +467,8 @@ function Copilot:fetch_models() return out end +--- Fetch agents from the Copilot API +---@return table function Copilot:fetch_agents() if self.agents then return self.agents @@ -826,7 +834,7 @@ function Copilot:ask(prompt, opts) end --- List available models ----@return table +---@return table function Copilot:list_models() local models = self:fetch_models() @@ -849,7 +857,7 @@ function Copilot:list_models() end --- List available agents ----@return table +---@return table function Copilot:list_agents() local agents = self:fetch_agents() @@ -866,6 +874,7 @@ end --- Generate embeddings for the given inputs ---@param inputs table: The inputs to embed ---@param opts CopilotChat.copilot.embed.opts: Options for the request +---@return table function Copilot:embed(inputs, opts) if not inputs or #inputs == 0 then return {} @@ -909,17 +918,17 @@ function Copilot:embed(inputs, opts) if err then error(err) - return + return {} end if not response then error('Failed to get response') - return + return {} end if response.status ~= 200 then error('Failed to get response: ' .. tostring(response.status) .. '\n' .. response.body) - return + return {} end local ok, content = pcall(vim.json.decode, response.body, { @@ -931,7 +940,7 @@ function Copilot:embed(inputs, opts) if not ok then error('Failed to parse response: ' .. vim.inspect(content) .. '\n' .. response.body) - return + return {} end for _, embedding in ipairs(content.data) do @@ -952,6 +961,7 @@ function Copilot:embed(inputs, opts) end --- Stop the running job +---@return boolean function Copilot:stop() if self.current_job ~= nil then self.current_job = nil @@ -962,6 +972,7 @@ function Copilot:stop() end --- Reset the history and stop any running job +---@return boolean function Copilot:reset() local stopped = self:stop() self.history = {} diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 28d217a1..7a515161 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1,15 +1,16 @@ -local default_config = require('CopilotChat.config') local async = require('plenary.async') local log = require('plenary.log') +local default_config = require('CopilotChat.config') local Copilot = require('CopilotChat.copilot') -local Chat = require('CopilotChat.chat') -local Diff = require('CopilotChat.diff') -local Overlay = require('CopilotChat.overlay') local context = require('CopilotChat.context') local prompts = require('CopilotChat.prompts') -local debuginfo = require('CopilotChat.debuginfo') local utils = require('CopilotChat.utils') +local Chat = require('CopilotChat.ui.chat') +local Diff = require('CopilotChat.ui.diff') +local Overlay = require('CopilotChat.ui.overlay') +local Debug = require('CopilotChat.ui.debug') + local M = {} local PLUGIN_NAME = 'CopilotChat.nvim' local WORD = '([^%s]+)' @@ -20,10 +21,10 @@ local WORD = '([^%s]+)' --- @field config CopilotChat.config? --- @field last_prompt string? --- @field last_response string? ---- @field chat CopilotChat.Chat? ---- @field diff CopilotChat.Diff? ---- @field overlay CopilotChat.Overlay? ---- @field help CopilotChat.Overlay? +--- @field chat CopilotChat.ui.Chat? +--- @field diff CopilotChat.ui.Diff? +--- @field debug CopilotChat.ui.Debug? +--- @field overlay CopilotChat.ui.Overlay? local state = { copilot = nil, @@ -39,6 +40,7 @@ local state = { chat = nil, diff = nil, overlay = nil, + debug = nil, } ---@param config CopilotChat.config @@ -102,7 +104,7 @@ local function update_selection() highlight_selection() end ----@return CopilotChat.Diff.diff|nil +---@return CopilotChat.ui.Diff.Diff? local function get_diff() local block = state.chat:get_closest_block() @@ -175,7 +177,7 @@ local function jump_to_diff(winnr, bufnr, start_line, end_line) update_selection() end ----@param diff CopilotChat.Diff.diff? +---@param diff CopilotChat.ui.Diff.Diff? local function apply_diff(diff) if not diff or not diff.bufnr then return @@ -633,7 +635,7 @@ function M.select_agent() end --- Ask a question to the Copilot model. ----@param prompt string +---@param prompt string? ---@param config CopilotChat.config|CopilotChat.config.prompt|nil function M.ask(prompt, config) config = vim.tbl_deep_extend('force', M.config, config or {}) @@ -916,6 +918,19 @@ function M.setup(config) diff_help = diff_help .. '\n' .. overlay_help end + if state.overlay then + state.overlay:delete() + end + state.overlay = Overlay('copilot-overlay', overlay_help, function(bufnr) + map_key(M.config.mappings.close, bufnr, function() + state.overlay:restore(state.chat.winnr, state.chat.bufnr) + end) + end) + + if not state.debug then + state.debug = Debug() + end + if state.diff then state.diff:delete() end @@ -929,15 +944,6 @@ function M.setup(config) end) end) - if state.overlay then - state.overlay:delete() - end - state.overlay = Overlay('copilot-overlay', overlay_help, function(bufnr) - map_key(M.config.mappings.close, bufnr, function() - state.overlay:restore(state.chat.winnr, state.chat.bufnr) - end) - end) - if state.chat then state.chat:close(state.source and state.source.bufnr or nil) state.chat:delete() @@ -972,7 +978,7 @@ function M.setup(config) end end end - state.overlay:show(chat_help, 'markdown', state.chat.winnr) + state.overlay:show(chat_help, state.chat.winnr, 'markdown') end) map_key(M.config.mappings.reset, bufnr, M.reset) @@ -1133,7 +1139,7 @@ function M.setup(config) return end - state.overlay:show(vim.trim(system_prompt) .. '\n', 'markdown', state.chat.winnr) + state.overlay:show(vim.trim(system_prompt) .. '\n', state.chat.winnr, 'markdown') end) map_key(M.config.mappings.show_user_selection, bufnr, function() @@ -1142,7 +1148,7 @@ function M.setup(config) return end - state.overlay:show(selection.content, selection.filetype, state.chat.winnr) + state.overlay:show(selection.content, state.chat.winnr, selection.filetype) end) map_key(M.config.mappings.show_user_context, bufnr, function() @@ -1165,7 +1171,7 @@ function M.setup(config) .. string.format('%s\n```%s\n%s\n```\n\n', header, embedding.filetype, preview) end - state.overlay:show(vim.trim(text) .. '\n', 'markdown', state.chat.winnr) + state.overlay:show(vim.trim(text) .. '\n', state.chat.winnr, 'markdown') end) vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { @@ -1265,7 +1271,7 @@ function M.setup(config) M.reset() end, { force = true }) vim.api.nvim_create_user_command('CopilotChatDebugInfo', function() - debuginfo.open() + state.debug:open() end, { force = true }) local function complete_load() diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/ui/chat.lua similarity index 89% rename from lua/CopilotChat/chat.lua rename to lua/CopilotChat/ui/chat.lua index 2457c34e..ced298da 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -1,25 +1,5 @@ ----@class CopilotChat.Chat ----@field bufnr number ----@field winnr number ----@field sections table ----@field get_closest_section fun(self: CopilotChat.Chat): table|nil ----@field get_closest_block fun(self: CopilotChat.Chat): table|nil ----@field clear_prompt fun(self: CopilotChat.Chat) ----@field valid fun(self: CopilotChat.Chat) ----@field visible fun(self: CopilotChat.Chat) ----@field active fun(self: CopilotChat.Chat) ----@field append fun(self: CopilotChat.Chat, str: string) ----@field last fun(self: CopilotChat.Chat) ----@field clear fun(self: CopilotChat.Chat) ----@field open fun(self: CopilotChat.Chat, config: CopilotChat.config) ----@field close fun(self: CopilotChat.Chat, bufnr: number?) ----@field focus fun(self: CopilotChat.Chat) ----@field follow fun(self: CopilotChat.Chat) ----@field finish fun(self: CopilotChat.Chat, msg: string?, offset: number?) ----@field delete fun(self: CopilotChat.Chat) - -local Overlay = require('CopilotChat.overlay') -local Spinner = require('CopilotChat.spinner') +local Overlay = require('CopilotChat.ui.overlay') +local Spinner = require('CopilotChat.ui.spinner') local utils = require('CopilotChat.utils') local is_stable = utils.is_stable local class = utils.class @@ -34,6 +14,7 @@ function CopilotChatFoldExpr(lnum, separator) return '=' end +---@param header? string ---@return string?, number?, number? local function match_header(header) if not header then @@ -56,6 +37,39 @@ local function match_header(header) return header_filename, header_start_line, header_end_line end +---@class CopilotChat.ui.Chat.Section.Block.Header +---@field filename string +---@field start_line number +---@field end_line number +---@field filetype string + +---@class CopilotChat.ui.Chat.Section.Block +---@field header CopilotChat.ui.Chat.Section.Block.Header +---@field start_line number +---@field end_line number +---@field content string? + +---@class CopilotChat.ui.Chat.Section +---@field answer boolean +---@field start_line number +---@field end_line number +---@field blocks table +---@field content string? + +---@class CopilotChat.ui.Chat : CopilotChat.ui.Overlay +---@field header_ns number +---@field winnr number? +---@field spinner CopilotChat.ui.Spinner +---@field sections table +---@field token_count number? +---@field token_max_count number? +---@field layout string? +---@field auto_insert boolean +---@field auto_follow_cursor boolean +---@field highlight_headers boolean +---@field question_header string? +---@field answer_header string? +---@field separator string? local Chat = class(function(self, help, on_buf_create) Overlay.init(self, 'copilot-chat', help, on_buf_create) vim.treesitter.language.register('markdown', self.name) @@ -79,6 +93,7 @@ local Chat = class(function(self, help, on_buf_create) self.separator = nil end, Overlay) +---@return number function Chat:create() local bufnr = Overlay.create(self) vim.bo[bufnr].syntax = 'markdown' @@ -113,10 +128,12 @@ function Chat:validate() end end +---@return boolean function Chat:visible() return self.winnr - and vim.api.nvim_win_is_valid(self.winnr) - and vim.api.nvim_win_get_buf(self.winnr) == self.bufnr + and vim.api.nvim_win_is_valid(self.winnr) + and vim.api.nvim_win_get_buf(self.winnr) == self.bufnr + or false end function Chat:render() @@ -220,6 +237,7 @@ function Chat:render() self.sections = sections end +---@return CopilotChat.ui.Chat.Section? function Chat:get_closest_section() if not self:visible() then return nil @@ -256,6 +274,7 @@ function Chat:get_closest_section() } end +---@return CopilotChat.ui.Chat.Section.Block? function Chat:get_closest_block() if not self:visible() then return nil @@ -311,10 +330,12 @@ function Chat:clear_prompt() vim.bo[self.bufnr].modifiable = false end +---@return boolean function Chat:active() return vim.api.nvim_get_current_win() == self.winnr end +---@return number, number, number function Chat:last() self:validate() local line_count = vim.api.nvim_buf_line_count(self.bufnr) @@ -330,6 +351,7 @@ function Chat:last() return last_line, last_column, line_count end +---@param str string function Chat:append(str) self:validate() vim.bo[self.bufnr].modifiable = true @@ -377,6 +399,7 @@ function Chat:clear() vim.bo[self.bufnr].modifiable = false end +---@param config CopilotChat.config function Chat:open(config) self:validate() @@ -475,6 +498,7 @@ function Chat:open(config) self:render() end +---@param bufnr number? function Chat:close(bufnr) if not self:visible() then return @@ -485,7 +509,9 @@ function Chat:close(bufnr) end if self.layout == 'replace' then - self:restore(self.winnr, bufnr) + if bufnr then + self:restore(self.winnr, bufnr) + end else vim.api.nvim_win_close(self.winnr, true) end diff --git a/lua/CopilotChat/debuginfo.lua b/lua/CopilotChat/ui/debug.lua similarity index 62% rename from lua/CopilotChat/debuginfo.lua rename to lua/CopilotChat/ui/debug.lua index 30827773..dd8db0a8 100644 --- a/lua/CopilotChat/debuginfo.lua +++ b/lua/CopilotChat/ui/debug.lua @@ -1,9 +1,19 @@ local log = require('plenary.log') local utils = require('CopilotChat.utils') local context = require('CopilotChat.context') -local M = {} +local Overlay = require('CopilotChat.ui.overlay') +local class = utils.class -function M.open() +---@class CopilotChat.ui.Debug : CopilotChat.ui.Overlay +local Debug = class(function(self) + Overlay.init(self, 'copilot-debug', nil, function(bufnr) + vim.keymap.set('n', 'q', function() + vim.api.nvim_win_close(0, true) + end, { buffer = bufnr }) + end) +end, Overlay) + +function Debug:get_debug_content() local lines = { 'If you are facing issues, run `:checkhealth CopilotChat` and share the output.', '', @@ -46,40 +56,45 @@ function M.open() table.insert(lines, '```') end + return lines +end + +function Debug:open() + self:validate() + + local lines = self:get_debug_content() + local height = math.min(vim.o.lines - 3, #lines) local width = 0 for _, line in ipairs(lines) do width = math.max(width, #line) end - local height = math.min(vim.o.lines - 3, #lines) - local opts = { + + local win_opts = { title = 'CopilotChat.nvim Debug Info', relative = 'editor', width = width, height = height, - row = (vim.o.lines - height) / 2 - 1, - col = (vim.o.columns - width) / 2, + row = math.floor((vim.o.lines - height) / 2) - 1, + col = math.floor((vim.o.columns - width) / 2), style = 'minimal', border = 'rounded', + zindex = 50, } if not utils.is_stable() then - opts.footer = "Press 'q' to close this window." + win_opts.footer = "Press 'q' to close this window." end - local bufnr = vim.api.nvim_create_buf(false, true) - vim.bo[bufnr].syntax = 'markdown' - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - vim.bo[bufnr].modifiable = false - vim.treesitter.start(bufnr, 'markdown') - - local win = vim.api.nvim_open_win(bufnr, true, opts) - vim.wo[win].wrap = true - vim.wo[win].linebreak = true - vim.wo[win].cursorline = true - vim.wo[win].conceallevel = 2 + -- Open window + local winnr = vim.api.nvim_open_win(self.bufnr, true, win_opts) + vim.wo[winnr].wrap = true + vim.wo[winnr].linebreak = true + vim.wo[winnr].cursorline = true + vim.wo[winnr].conceallevel = 2 - -- Bind 'q' to close the window - vim.api.nvim_buf_set_keymap(bufnr, 'n', 'q', 'close', { noremap = true, silent = true }) + -- Show content + self:show(table.concat(lines, '\n'), winnr, 'markdown') + vim.api.nvim_win_set_cursor(winnr, { 1, 0 }) end -return M +return Debug diff --git a/lua/CopilotChat/diff.lua b/lua/CopilotChat/ui/diff.lua similarity index 78% rename from lua/CopilotChat/diff.lua rename to lua/CopilotChat/ui/diff.lua index 2ee24bc3..4cf92805 100644 --- a/lua/CopilotChat/diff.lua +++ b/lua/CopilotChat/ui/diff.lua @@ -1,4 +1,8 @@ ----@class CopilotChat.Diff.diff +local Overlay = require('CopilotChat.ui.overlay') +local utils = require('CopilotChat.utils') +local class = utils.class + +---@class CopilotChat.ui.Diff.Diff ---@field change string ---@field reference string ---@field filename string @@ -7,17 +11,9 @@ ---@field end_line number ---@field bufnr number? ----@class CopilotChat.Diff ----@field bufnr number ----@field show fun(self: CopilotChat.Diff, diff: CopilotChat.Diff.diff, winnr: number) ----@field restore fun(self: CopilotChat.Diff, winnr: number, bufnr: number) ----@field delete fun(self: CopilotChat.Diff) ----@field get_diff fun(self: CopilotChat.Diff): CopilotChat.Diff.diff - -local Overlay = require('CopilotChat.overlay') -local utils = require('CopilotChat.utils') -local class = utils.class - +---@class CopilotChat.ui.Diff : CopilotChat.ui.Overlay +---@field hl_ns number +---@field diff CopilotChat.ui.Diff.Diff? local Diff = class(function(self, help, on_buf_create) Overlay.init(self, 'copilot-diff', help, on_buf_create) self.hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights') @@ -28,6 +24,8 @@ local Diff = class(function(self, help, on_buf_create) self.diff = nil end, Overlay) +---@param diff CopilotChat.ui.Diff.Diff +---@param winnr number function Diff:show(diff, winnr) self.diff = diff self:validate() @@ -45,17 +43,20 @@ function Diff:show(diff, winnr) algorithm = 'myers', ctxlen = #diff.reference, })), - diff.filetype, winnr, + diff.filetype, 'diff' ) end +---@param winnr number +---@param bufnr number function Diff:restore(winnr, bufnr) Overlay.restore(self, winnr, bufnr) vim.api.nvim_win_set_hl_ns(winnr, 0) end +---@return CopilotChat.ui.Diff.Diff? function Diff:get_diff() return self.diff end diff --git a/lua/CopilotChat/overlay.lua b/lua/CopilotChat/ui/overlay.lua similarity index 78% rename from lua/CopilotChat/overlay.lua rename to lua/CopilotChat/ui/overlay.lua index 10808e22..2456d767 100644 --- a/lua/CopilotChat/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -1,18 +1,12 @@ ----@class CopilotChat.Overlay ----@field name string ----@field bufnr number ----@field create fun(self: CopilotChat.Overlay) ----@field valid fun(self: CopilotChat.Overlay) ----@field validate fun(self: CopilotChat.Overlay) ----@field show fun(self: CopilotChat.Overlay, text: string, filetype: string?, winnr: number) ----@field restore fun(self: CopilotChat.Overlay, winnr: number, bufnr: number) ----@field delete fun(self: CopilotChat.Overlay) ----@field show_help fun(self: CopilotChat.Overlay, msg: string, offset: number) ----@field clear_help fun(self: CopilotChat.Overlay) - local utils = require('CopilotChat.utils') local class = utils.class +---@class CopilotChat.ui.Overlay : CopilotChat.utils.Class +---@field name string +---@field help string +---@field help_ns number +---@field on_buf_create fun(bufnr: number) +---@field bufnr number? local Overlay = class(function(self, name, help, on_buf_create) self.name = name self.help = help @@ -21,6 +15,7 @@ local Overlay = class(function(self, name, help, on_buf_create) self.bufnr = nil end) +---@return number function Overlay:create() local bufnr = vim.api.nvim_create_buf(false, true) vim.bo[bufnr].filetype = self.name @@ -29,6 +24,7 @@ function Overlay:create() return bufnr end +---@return boolean function Overlay:valid() return utils.buf_valid(self.bufnr) end @@ -39,10 +35,16 @@ function Overlay:validate() end self.bufnr = self:create() - self.on_buf_create(self.bufnr) + if self.on_buf_create then + self.on_buf_create(self.bufnr) + end end -function Overlay:show(text, filetype, winnr, syntax) +---@param text string +---@param winnr number +---@param filetype? string +---@param syntax string? +function Overlay:show(text, winnr, filetype, syntax) if not text or vim.trim(text) == '' then return end @@ -70,6 +72,8 @@ function Overlay:show(text, filetype, winnr, syntax) end end +---@param winnr number +---@param bufnr number? function Overlay:restore(winnr, bufnr) vim.api.nvim_win_set_buf(winnr, bufnr or 0) end @@ -80,6 +84,8 @@ function Overlay:delete() end end +---@param msg string +---@param offset number function Overlay:show_help(msg, offset) if not msg then return diff --git a/lua/CopilotChat/spinner.lua b/lua/CopilotChat/ui/spinner.lua similarity index 86% rename from lua/CopilotChat/spinner.lua rename to lua/CopilotChat/ui/spinner.lua index b252a986..13667cf0 100644 --- a/lua/CopilotChat/spinner.lua +++ b/lua/CopilotChat/ui/spinner.lua @@ -1,9 +1,3 @@ ----@class CopilotChat.Spinner ----@field bufnr number ----@field set fun(self: CopilotChat.Spinner, text: string, virt_line: boolean) ----@field start fun(self: CopilotChat.Spinner) ----@field finish fun(self: CopilotChat.Spinner) - local utils = require('CopilotChat.utils') local class = utils.class @@ -20,6 +14,11 @@ local spinner_frames = { '⠏', } +---@class CopilotChat.ui.Spinner : CopilotChat.utils.Class +---@field ns number +---@field bufnr number +---@field timer table +---@field index number local Spinner = class(function(self, bufnr) self.ns = vim.api.nvim_create_namespace('copilot-chat-spinner') self.bufnr = bufnr diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 10698e77..ef3a02b2 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,10 +1,14 @@ local M = {} M.timers = {} +---@class CopilotChat.utils.Class +---@field new fun(...):table +---@field init fun(self, ...) + --- Create class ---@param fn function The class constructor ---@param parent table? The parent class ----@return table +---@return CopilotChat.utils.Class function M.class(fn, parent) local out = {} out.__index = out @@ -168,7 +172,7 @@ function M.quick_hash(str) end --- Get current working directory for target window ----@param winnr number The buffer number +---@param winnr number? The buffer number ---@return string function M.win_cwd(winnr) if not winnr or not vim.api.nvim_win_is_valid(winnr) then @@ -192,7 +196,7 @@ end --- Create an ordered map ---@return OrderedMap function M.ordered_map() - local ordered_map = { + return { _keys = {}, _data = {}, set = function(self, key, value) @@ -218,8 +222,6 @@ function M.ordered_map() return result end, } - - return ordered_map end return M From 9949c2108cb7b2095004bb860ac551c17f772504 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 23 Nov 2024 00:14:28 +0100 Subject: [PATCH 258/338] refactor: extract debug info content retrieval outside of class Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 7 ++----- lua/CopilotChat/ui/debug.lua | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7a515161..16d1c770 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1189,7 +1189,7 @@ function M.setup(config) if M.config.insert_at_end then vim.api.nvim_create_autocmd({ 'InsertEnter' }, { - buffer = state.chat.bufnr, + buffer = bufnr, callback = function() vim.cmd('normal! 0') vim.cmd('normal! G$') @@ -1285,7 +1285,6 @@ function M.setup(config) return options end - vim.api.nvim_create_user_command('CopilotChatSave', function(args) M.save(args.args) end, { nargs = '*', force = true, complete = complete_load }) @@ -1293,14 +1292,12 @@ function M.setup(config) M.load(args.args) end, { nargs = '*', force = true, complete = complete_load }) - local augroup = vim.api.nvim_create_augroup('CopilotChat', {}) - -- Store the current directory to window when directory changes -- I dont think there is a better way to do this that functions -- with "rooter" plugins, LSP and stuff as vim.fn.getcwd() when -- i pass window number inside doesnt work vim.api.nvim_create_autocmd({ 'VimEnter', 'WinEnter', 'DirChanged' }, { - group = augroup, + group = vim.api.nvim_create_augroup('CopilotChat', {}), callback = function() vim.w.cchat_cwd = vim.fn.getcwd() end, diff --git a/lua/CopilotChat/ui/debug.lua b/lua/CopilotChat/ui/debug.lua index dd8db0a8..a09ec7cd 100644 --- a/lua/CopilotChat/ui/debug.lua +++ b/lua/CopilotChat/ui/debug.lua @@ -4,16 +4,8 @@ local context = require('CopilotChat.context') local Overlay = require('CopilotChat.ui.overlay') local class = utils.class ----@class CopilotChat.ui.Debug : CopilotChat.ui.Overlay -local Debug = class(function(self) - Overlay.init(self, 'copilot-debug', nil, function(bufnr) - vim.keymap.set('n', 'q', function() - vim.api.nvim_win_close(0, true) - end, { buffer = bufnr }) - end) -end, Overlay) - -function Debug:get_debug_content() +---@return table +local function build_debug_info() local lines = { 'If you are facing issues, run `:checkhealth CopilotChat` and share the output.', '', @@ -59,10 +51,19 @@ function Debug:get_debug_content() return lines end +---@class CopilotChat.ui.Debug : CopilotChat.ui.Overlay +local Debug = class(function(self) + Overlay.init(self, 'copilot-debug', nil, function(bufnr) + vim.keymap.set('n', 'q', function() + vim.api.nvim_win_close(0, true) + end, { buffer = bufnr }) + end) +end, Overlay) + function Debug:open() self:validate() - local lines = self:get_debug_content() + local lines = build_debug_info() local height = math.min(vim.o.lines - 3, #lines) local width = 0 for _, line in ipairs(lines) do From f29c515240f073e6c722cf46f82849ec6eb99bf8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 23 Nov 2024 00:36:48 +0100 Subject: [PATCH 259/338] fix(selection): properly pass chat config to functions Pass chat configuration to functions that were previously using global state config, making the code more reliable and avoiding potential race conditions when multiple chat instances are running simultaneously. --- lua/CopilotChat/init.lua | 52 ++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 16d1c770..d089605f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -56,15 +56,16 @@ local function get_selection(config) and winnr and vim.api.nvim_win_is_valid(winnr) then - return state.config.selection(state.source) + return config.selection(state.source) end return nil end --- Highlights the selection in the source buffer. ----@param clear? boolean -local function highlight_selection(clear) +---@param clear boolean +---@param config CopilotChat.config +local function highlight_selection(clear, config) local selection_ns = vim.api.nvim_create_namespace('copilot-chat-selection') for _, buf in ipairs(vim.api.nvim_list_bufs()) do vim.api.nvim_buf_clear_namespace(buf, selection_ns, 0, -1) @@ -74,7 +75,7 @@ local function highlight_selection(clear) return end - local selection = get_selection(state.config) + local selection = get_selection(config) if not selection or not utils.buf_valid(selection.bufnr) @@ -92,7 +93,8 @@ local function highlight_selection(clear) end --- Updates the selection based on previous window -local function update_selection() +---@param config CopilotChat.config +local function update_selection(config) local prev_winnr = vim.fn.win_getid(vim.fn.winnr('#')) if prev_winnr ~= state.chat.winnr and vim.fn.win_gettype(prev_winnr) == '' then state.source = { @@ -101,11 +103,12 @@ local function update_selection() } end - highlight_selection() + highlight_selection(false, config) end +---@param config CopilotChat.config ---@return CopilotChat.ui.Diff.Diff? -local function get_diff() +local function get_diff(config) local block = state.chat:get_closest_block() -- If no block found, return nil @@ -115,7 +118,7 @@ local function get_diff() -- Initialize variables with selection if available local header = block.header - local selection = get_selection(state.config) + local selection = get_selection(config) local reference = selection and selection.content local start_line = selection and selection.start_line local end_line = selection and selection.end_line @@ -168,17 +171,19 @@ end ---@param bufnr number ---@param start_line number ---@param end_line number -local function jump_to_diff(winnr, bufnr, start_line, end_line) +---@param config CopilotChat.config +local function jump_to_diff(winnr, bufnr, start_line, end_line, config) vim.api.nvim_win_set_cursor(winnr, { start_line, 0 }) pcall(vim.api.nvim_buf_set_mark, bufnr, '<', start_line, 0, {}) pcall(vim.api.nvim_buf_set_mark, bufnr, '>', end_line, 0, {}) pcall(vim.api.nvim_buf_set_mark, bufnr, '[', start_line, 0, {}) pcall(vim.api.nvim_buf_set_mark, bufnr, ']', end_line, 0, {}) - update_selection() + update_selection(config) end ---@param diff CopilotChat.ui.Diff.Diff? -local function apply_diff(diff) +---@param config CopilotChat.config +local function apply_diff(diff, config) if not diff or not diff.bufnr then return end @@ -190,7 +195,7 @@ local function apply_diff(diff) local lines = vim.split(diff.change, '\n', { trimempty = false }) vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines) - jump_to_diff(winnr, diff.bufnr, diff.start_line, diff.start_line + #lines - 1) + jump_to_diff(winnr, diff.bufnr, diff.start_line, diff.start_line + #lines - 1, config) end ---@param prompt string @@ -301,9 +306,10 @@ local function finish(config, start_of_chat) end ---@param config CopilotChat.config ----@param err string|table +---@param err string|table|nil ---@param append_newline boolean? local function show_error(config, err, append_newline) + err = err or 'Unknown error' log.error(vim.inspect(err)) if config.no_chat then @@ -940,7 +946,7 @@ function M.setup(config) end) map_key(M.config.mappings.accept_diff, bufnr, function() - apply_diff(state.diff:get_diff()) + apply_diff(state.diff:get_diff(), state.config) end) end) @@ -991,7 +997,7 @@ function M.setup(config) return end - M.ask(section.content, state.config) + M.ask(section.content) end) map_key(M.config.mappings.toggle_sticky, bufnr, function() @@ -1039,7 +1045,7 @@ function M.setup(config) end) map_key(M.config.mappings.accept_diff, bufnr, function() - apply_diff(get_diff()) + apply_diff(get_diff(state.config), state.config) end) map_key(M.config.mappings.jump_to_diff, bufnr, function() @@ -1051,7 +1057,7 @@ function M.setup(config) return end - local diff = get_diff() + local diff = get_diff(state.config) if not diff then return end @@ -1076,7 +1082,7 @@ function M.setup(config) end vim.api.nvim_win_set_buf(state.source.winnr, diff_bufnr) - jump_to_diff(state.source.winnr, diff_bufnr, diff.start_line, diff.end_line) + jump_to_diff(state.source.winnr, diff_bufnr, diff.start_line, diff.end_line, state.config) end) map_key(M.config.mappings.quickfix_diffs, bufnr, function() @@ -1112,7 +1118,7 @@ function M.setup(config) end) map_key(M.config.mappings.yank_diff, bufnr, function() - local diff = get_diff() + local diff = get_diff(state.config) if not diff then return end @@ -1121,7 +1127,7 @@ function M.setup(config) end) map_key(M.config.mappings.show_diff, bufnr, function() - local diff = get_diff() + local diff = get_diff(state.config) if not diff then return end @@ -1133,7 +1139,7 @@ function M.setup(config) local section = state.chat:get_closest_section() local system_prompt = state.config.system_prompt if section and not section.answer then - system_prompt = resolve_prompts(section.content, state.config.system_prompt) + system_prompt = resolve_prompts(section.content, system_prompt) end if not system_prompt then return @@ -1180,9 +1186,9 @@ function M.setup(config) local is_enter = ev.event == 'BufEnter' if is_enter then - update_selection() + update_selection(state.config) else - highlight_selection(true) + highlight_selection(true, state.config) end end, }) From 36d0c653972a0de74a4feb83c85c5e97968eecb1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 23 Nov 2024 11:30:10 +0100 Subject: [PATCH 260/338] refactor: improve prompt resolution handling and cleanup types Rewrite prompt resolution to use recursive approach with depth limiting to prevent infinite loops. Move shared config types to separate class and improve type definitions. Clean up prompt handling by removing kind field and simplifying system/user prompt distinction. Fix order of returned values in resolve_prompts to match usage. Also includes some code formatting fixes and reordering of config options for better readability. Signed-off-by: Tomas Slusny --- README.md | 52 +++++++-------- lua/CopilotChat/actions.lua | 6 +- lua/CopilotChat/config.lua | 65 +++++++++---------- lua/CopilotChat/init.lua | 122 ++++++++++++++++++------------------ 4 files changed, 125 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index cfa249bf..ca6bc77c 100644 --- a/README.md +++ b/README.md @@ -443,18 +443,33 @@ Also see [here](/lua/CopilotChat/config.lua): auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt insert_at_end = false, -- Move cursor to end of buffer when inserting text clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - highlight_selection = true, -- Highlight selection in the source buffer when in the chat window + highlight_selection = true, -- Highlight selection highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history + no_chat = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) callback = nil, -- Callback to use when ask response is received - no_chat = false, -- Do not write to chat buffer and use chat history (useful for using callback for custom processing) -- default selection selection = function(source) return select.visual(source) or select.buffer(source) end, + -- default window options + window = { + layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace' + width = 0.5, -- fractional width of parent, or absolute width in columns when > 1 + height = 0.5, -- fractional height of parent, or absolute height in rows when > 1 + -- Options below only apply to floating windows + relative = 'editor', -- 'editor', 'win', 'cursor', 'mouse' + border = 'single', -- 'none', single', 'double', 'rounded', 'solid', 'shadow' + row = nil, -- row position of the window, default is centered + col = nil, -- column position of the window, default is centered + title = 'Copilot Chat', -- title of chat window + footer = nil, -- footer of chat window + zindex = 1, -- determines if window is on top or below other floating windows + }, + -- default contexts contexts = { buffer = { @@ -503,37 +518,22 @@ Also see [here](/lua/CopilotChat/config.lua): }, }, - -- default window options - window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace' - width = 0.5, -- fractional width of parent, or absolute width in columns when > 1 - height = 0.5, -- fractional height of parent, or absolute height in rows when > 1 - -- Options below only apply to floating windows - relative = 'editor', -- 'editor', 'win', 'cursor', 'mouse' - border = 'single', -- 'none', single', 'double', 'rounded', 'solid', 'shadow' - row = nil, -- row position of the window, default is centered - col = nil, -- column position of the window, default is centered - title = 'Copilot Chat', -- title of chat window - footer = nil, -- footer of chat window - zindex = 1, -- determines if window is on top or below other floating windows - }, - -- default mappings mappings = { complete = { - insert ='', + insert = '', }, close = { normal = 'q', - insert = '' + insert = '', }, reset = { - normal ='', - insert = '' + normal = '', + insert = '', }, submit_prompt = { normal = '', - insert = '' + insert = '', }, toggle_sticky = { detail = 'Makes line under cursor sticky or deletes sticky line.', @@ -541,7 +541,7 @@ Also see [here](/lua/CopilotChat/config.lua): }, accept_diff = { normal = '', - insert = '' + insert = '', }, jump_to_diff = { normal = 'gj', @@ -554,13 +554,13 @@ Also see [here](/lua/CopilotChat/config.lua): register = '"', }, show_diff = { - normal = 'gd' + normal = 'gd', }, show_system_prompt = { - normal = 'gp' + normal = 'gp', }, show_user_selection = { - normal = 'gs' + normal = 'gs', }, show_user_context = { normal = 'gc', diff --git a/lua/CopilotChat/actions.lua b/lua/CopilotChat/actions.lua index 4ae5dfeb..a682e5ae 100644 --- a/lua/CopilotChat/actions.lua +++ b/lua/CopilotChat/actions.lua @@ -17,8 +17,10 @@ end ---@return CopilotChat.integrations.actions?: The prompt actions function M.prompt_actions(config) local actions = {} - for name, prompt in pairs(chat.prompts(true)) do - actions[name] = vim.tbl_extend('keep', prompt, config or {}) + for name, prompt in pairs(chat.prompts()) do + if prompt.prompt then + actions[name] = vim.tbl_extend('keep', prompt, config or {}) + end end return { prompt = 'Copilot Chat Prompt Actions', diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 2605ed33..ca1d46ce 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -27,14 +27,10 @@ local utils = require('CopilotChat.utils') ---@field input fun(callback: fun(input: string?), source: CopilotChat.config.source)? ---@field resolve fun(input: string?, source: CopilotChat.config.source):table ----@class CopilotChat.config.prompt +---@class CopilotChat.config.prompt : CopilotChat.config.shared ---@field prompt string? ---@field description string? ----@field kind string? ---@field mapping string? ----@field system_prompt string? ----@field callback fun(response: string, source: CopilotChat.config.source)? ----@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.config.selection? ---@class CopilotChat.config.window ---@field layout string? @@ -53,6 +49,9 @@ local utils = require('CopilotChat.utils') ---@field insert string? ---@field detail string? +---@class CopilotChat.config.mapping.register : CopilotChat.config.mapping +---@field register string? + ---@class CopilotChat.config.mappings ---@field complete CopilotChat.config.mapping? ---@field close CopilotChat.config.mapping? @@ -62,19 +61,14 @@ local utils = require('CopilotChat.utils') ---@field accept_diff CopilotChat.config.mapping? ---@field jump_to_diff CopilotChat.config.mapping? ---@field quickfix_diffs CopilotChat.config.mapping? ----@field yank_diff CopilotChat.config.mapping? +---@field yank_diff CopilotChat.config.mapping.register? ---@field show_diff CopilotChat.config.mapping? ---@field show_system_prompt CopilotChat.config.mapping? ---@field show_user_selection CopilotChat.config.mapping? ---@field show_user_context CopilotChat.config.mapping? ---@field show_help CopilotChat.config.mapping? ---- CopilotChat default configuration ----@class CopilotChat.config ----@field debug boolean? ----@field log_level string? ----@field proxy string? ----@field allow_insecure boolean? +---@class CopilotChat.config.shared ---@field system_prompt string? ---@field model string? ---@field agent string? @@ -84,7 +78,6 @@ local utils = require('CopilotChat.utils') ---@field answer_header string? ---@field error_header string? ---@field separator string? ----@field chat_autocomplete boolean? ---@field show_folds boolean? ---@field show_help boolean? ---@field auto_follow_cursor boolean? @@ -92,13 +85,21 @@ local utils = require('CopilotChat.utils') ---@field clear_chat_on_new_prompt boolean? ---@field highlight_selection boolean? ---@field highlight_headers boolean? ----@field history_path string? ----@field callback fun(response: string, source: CopilotChat.config.source)? ---@field no_chat boolean? +---@field callback fun(response: string, source: CopilotChat.config.source)? ---@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.config.selection? +---@field window CopilotChat.config.window? + +--- CopilotChat default configuration +---@class CopilotChat.config : CopilotChat.config.shared +---@field debug boolean? +---@field log_level string? +---@field proxy string? +---@field allow_insecure boolean? +---@field chat_autocomplete boolean? +---@field history_path string? ---@field contexts table? ---@field prompts table? ----@field window CopilotChat.config.window? ---@field mappings CopilotChat.config.mappings? return { debug = false, -- Enable debug logging (same as 'log_level = 'debug') @@ -128,14 +129,29 @@ return { highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - callback = nil, -- Callback to use when ask response is received no_chat = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) + callback = nil, -- Callback to use when ask response is received -- default selection selection = function(source) return select.visual(source) or select.buffer(source) end, + -- default window options + window = { + layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace' + width = 0.5, -- fractional width of parent, or absolute width in columns when > 1 + height = 0.5, -- fractional height of parent, or absolute height in rows when > 1 + -- Options below only apply to floating windows + relative = 'editor', -- 'editor', 'win', 'cursor', 'mouse' + border = 'single', -- 'none', single', 'double', 'rounded', 'solid', 'shadow' + row = nil, -- row position of the window, default is centered + col = nil, -- column position of the window, default is centered + title = 'Copilot Chat', -- title of chat window + footer = nil, -- footer of chat window + zindex = 1, -- determines if window is on top or below other floating windows + }, + -- default contexts contexts = { buffer = { @@ -333,21 +349,6 @@ return { }, }, - -- default window options - window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace' - width = 0.5, -- fractional width of parent, or absolute width in columns when > 1 - height = 0.5, -- fractional height of parent, or absolute height in rows when > 1 - -- Options below only apply to floating windows - relative = 'editor', -- 'editor', 'win', 'cursor', 'mouse' - border = 'single', -- 'none', single', 'double', 'rounded', 'solid', 'shadow' - row = nil, -- row position of the window, default is centered - col = nil, -- column position of the window, default is centered - title = 'Copilot Chat', -- title of chat window - footer = nil, -- footer of chat window - zindex = 1, -- determines if window is on top or below other floating windows - }, - -- default mappings mappings = { complete = { diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d089605f..1dae1f05 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -203,31 +203,32 @@ end ---@return string, string local function resolve_prompts(prompt, system_prompt) local prompts_to_use = M.prompts() - local try_again = false - local result = string.gsub(prompt, '/' .. WORD, function(match) - local matched_prompt = prompts_to_use[match] - if matched_prompt then - if matched_prompt.kind == 'user' then - local out = matched_prompt.prompt - if out and string.match(out, '/' .. WORD) then - try_again = true - end - system_prompt = matched_prompt.system_prompt or system_prompt - return out - elseif matched_prompt.kind == 'system' then - system_prompt = matched_prompt.prompt - return '' - end + local depth = 0 + local MAX_DEPTH = 10 + + local function resolve(inner_prompt, inner_system_prompt) + if depth >= MAX_DEPTH then + return inner_prompt, inner_system_prompt end + depth = depth + 1 + + inner_prompt = string.gsub(inner_prompt, '/' .. WORD, function(match) + local p = prompts_to_use[match] + if p then + local resolved_prompt, resolved_system_prompt = + resolve(p.prompt or '', p.system_prompt or inner_system_prompt) + inner_system_prompt = resolved_system_prompt + return resolved_prompt + end - return '/' .. match - end) + return '/' .. match + end) - if try_again then - return resolve_prompts(result, system_prompt) + depth = depth - 1 + return inner_prompt, inner_system_prompt end - return system_prompt, result + return resolve(prompt, system_prompt) end ---@param prompt string @@ -466,10 +467,20 @@ function M.complete_items(callback) local items = {} for name, prompt in pairs(prompts_to_use) do + local kind = '' + local info = '' + if prompt.prompt then + kind = 'user' + info = prompt.prompt + elseif prompt.system_prompt then + kind = 'system' + info = prompt.system_prompt + end + items[#items + 1] = { word = '/' .. name, - kind = prompt.kind, - info = prompt.prompt, + kind = kind, + info = info, menu = prompt.description or '', icase = 1, dup = 0, @@ -521,22 +532,14 @@ function M.complete_items(callback) end --- Get the prompts to use. ----@param skip_system boolean|nil ---@return table -function M.prompts(skip_system) - local function get_prompt_kind(name) - return vim.startswith(name, 'COPILOT_') and 'system' or 'user' - end - +function M.prompts() local prompts_to_use = {} - if not skip_system then - for name, prompt in pairs(prompts) do - prompts_to_use[name] = { - prompt = prompt, - kind = get_prompt_kind(name), - } - end + for name, prompt in pairs(prompts) do + prompts_to_use[name] = { + system_prompt = prompt, + } end for name, prompt in pairs(M.config.prompts) do @@ -544,10 +547,7 @@ function M.prompts(skip_system) if type(prompt) == 'string' then val = { prompt = prompt, - kind = get_prompt_kind(name), } - elseif not val.kind then - val.kind = get_prompt_kind(name) end prompts_to_use[name] = val @@ -670,7 +670,7 @@ function M.ask(prompt, config) end -- Resolve prompt references - local system_prompt, resolved_prompt = resolve_prompts(prompt, config.system_prompt) + local resolved_prompt, system_prompt = resolve_prompts(prompt, config.system_prompt) -- Remove sticky prefix prompt = table.concat( @@ -856,7 +856,7 @@ function M.log_level(level) end --- Set up the plugin ----@param config CopilotChat.config|nil +---@param config CopilotChat.config? function M.setup(config) -- Handle changed configuration if config then @@ -1139,7 +1139,7 @@ function M.setup(config) local section = state.chat:get_closest_section() local system_prompt = state.config.system_prompt if section and not section.answer then - system_prompt = resolve_prompts(section.content, system_prompt) + _, system_prompt = resolve_prompts(section.content, system_prompt) end if not system_prompt then return @@ -1224,26 +1224,28 @@ function M.setup(config) end ) - for name, prompt in pairs(M.prompts(true)) do - vim.api.nvim_create_user_command('CopilotChat' .. name, function(args) - local input = prompt.prompt - if args.args and vim.trim(args.args) ~= '' then - input = input .. ' ' .. args.args - end - if input then - M.ask(input, prompt) + for name, prompt in pairs(M.prompts()) do + if prompt.prompt then + vim.api.nvim_create_user_command('CopilotChat' .. name, function(args) + local input = prompt.prompt + if args.args and vim.trim(args.args) ~= '' then + input = input .. ' ' .. args.args + end + if input then + M.ask(input, prompt) + end + end, { + nargs = '*', + force = true, + range = true, + desc = prompt.description or (PLUGIN_NAME .. ' ' .. name), + }) + + if prompt.mapping then + vim.keymap.set({ 'n', 'v' }, prompt.mapping, function() + M.ask(prompt.prompt, prompt) + end, { desc = prompt.description or (PLUGIN_NAME .. ' ' .. name) }) end - end, { - nargs = '*', - force = true, - range = true, - desc = prompt.description or (PLUGIN_NAME .. ' ' .. name), - }) - - if prompt.mapping then - vim.keymap.set({ 'n', 'v' }, prompt.mapping, function() - M.ask(prompt.prompt, prompt) - end, { desc = prompt.description or (PLUGIN_NAME .. ' ' .. name) }) end end From e75eaaaac22cefe52b2c5bd13ff83c35c38525df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 23 Nov 2024 10:35:13 +0000 Subject: [PATCH 261/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 54 ++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 1ea381b7..e1f47654 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 22 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 23 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -494,18 +494,33 @@ Also see here : auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt insert_at_end = false, -- Move cursor to end of buffer when inserting text clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - highlight_selection = true, -- Highlight selection in the source buffer when in the chat window + highlight_selection = true, -- Highlight selection highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history + no_chat = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) callback = nil, -- Callback to use when ask response is received - no_chat = false, -- Do not write to chat buffer and use chat history (useful for using callback for custom processing) -- default selection selection = function(source) return select.visual(source) or select.buffer(source) end, + -- default window options + window = { + layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace' + width = 0.5, -- fractional width of parent, or absolute width in columns when > 1 + height = 0.5, -- fractional height of parent, or absolute height in rows when > 1 + -- Options below only apply to floating windows + relative = 'editor', -- 'editor', 'win', 'cursor', 'mouse' + border = 'single', -- 'none', single', 'double', 'rounded', 'solid', 'shadow' + row = nil, -- row position of the window, default is centered + col = nil, -- column position of the window, default is centered + title = 'Copilot Chat', -- title of chat window + footer = nil, -- footer of chat window + zindex = 1, -- determines if window is on top or below other floating windows + }, + -- default contexts contexts = { buffer = { @@ -554,37 +569,22 @@ Also see here : }, }, - -- default window options - window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace' - width = 0.5, -- fractional width of parent, or absolute width in columns when > 1 - height = 0.5, -- fractional height of parent, or absolute height in rows when > 1 - -- Options below only apply to floating windows - relative = 'editor', -- 'editor', 'win', 'cursor', 'mouse' - border = 'single', -- 'none', single', 'double', 'rounded', 'solid', 'shadow' - row = nil, -- row position of the window, default is centered - col = nil, -- column position of the window, default is centered - title = 'Copilot Chat', -- title of chat window - footer = nil, -- footer of chat window - zindex = 1, -- determines if window is on top or below other floating windows - }, - -- default mappings mappings = { complete = { - insert ='', + insert = '', }, close = { normal = 'q', - insert = '' + insert = '', }, reset = { - normal ='', - insert = '' + normal = '', + insert = '', }, submit_prompt = { normal = '', - insert = '' + insert = '', }, toggle_sticky = { detail = 'Makes line under cursor sticky or deletes sticky line.', @@ -592,7 +592,7 @@ Also see here : }, accept_diff = { normal = '', - insert = '' + insert = '', }, jump_to_diff = { normal = 'gj', @@ -605,13 +605,13 @@ Also see here : register = '"', }, show_diff = { - normal = 'gd' + normal = 'gd', }, show_system_prompt = { - normal = 'gp' + normal = 'gp', }, show_user_selection = { - normal = 'gs' + normal = 'gs', }, show_user_context = { normal = 'gc', From b7c4d3b680ff01ae1ebb15c84a8b2724b7673a56 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 23 Nov 2024 11:42:21 +0100 Subject: [PATCH 262/338] feat: sort completion items by kind and word Sort completion items consistently by first comparing their kinds, and when kinds are equal, sort them alphabetically by word. This provides a more predictable and user-friendly order in completion menus. --- lua/CopilotChat/init.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 1dae1f05..c1938fa0 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -522,6 +522,9 @@ function M.complete_items(callback) end table.sort(items, function(a, b) + if a.kind == b.kind then + return a.word < b.word + end return a.kind < b.kind end) From 067de70be4f624fa1c61db5d49265cff9b9f9d47 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 24 Nov 2024 14:45:45 +0100 Subject: [PATCH 263/338] refactor(config): split config into static and shared parts properly Split the plugin configuration into two parts: - Static config that can only be set via setup() - Shared config that can be passed as runtime parameters to functions This improves the configuration management by clearly separating configuration parameters that should be set only once during setup from those that can be modified during runtime. Also renamed no_chat option to headless for better clarity. --- README.md | 48 +++++++----- lua/CopilotChat/actions.lua | 2 +- lua/CopilotChat/config.lua | 75 ++++++++++-------- lua/CopilotChat/init.lua | 151 ++++++++++++++++++++---------------- lua/CopilotChat/ui/chat.lua | 70 ++++------------- 5 files changed, 168 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index ca6bc77c..1432584c 100644 --- a/README.md +++ b/README.md @@ -420,10 +420,8 @@ Also see [here](/lua/CopilotChat/config.lua): ```lua { - debug = false, -- Enable debug logging (same as 'log_level = 'debug') - log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' - proxy = nil, -- [protocol://]host[:port] Use this proxy - allow_insecure = false, -- Allow insecure server connections + + -- Shared config starts here (can be passed to functions at runtime) system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). @@ -431,23 +429,7 @@ Also see [here](/lua/CopilotChat/config.lua): context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature - question_header = '## User ', -- Header to use for user questions - answer_header = '## Copilot ', -- Header to use for AI answers - error_header = '## Error ', -- Header to use for errors - separator = '───', -- Separator to use in chat - - chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) - show_folds = true, -- Shows folds for sections in chat - show_help = true, -- Shows help message as virtual lines when waiting for user input - auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt - insert_at_end = false, -- Move cursor to end of buffer when inserting text - clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) - - history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - no_chat = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) + headless = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) callback = nil, -- Callback to use when ask response is received -- default selection @@ -470,6 +452,30 @@ Also see [here](/lua/CopilotChat/config.lua): zindex = 1, -- determines if window is on top or below other floating windows }, + -- Static config starts here (can be configured only via setup function) + + debug = false, -- Enable debug logging (same as 'log_level = 'debug') + log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' + proxy = nil, -- [protocol://]host[:port] Use this proxy + allow_insecure = false, -- Allow insecure server connections + history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history + + question_header = '## User ', -- Header to use for user questions + answer_header = '## Copilot ', -- Header to use for AI answers + error_header = '## Error ', -- Header to use for errors + separator = '───', -- Separator to use in chat + + show_folds = true, -- Shows folds for sections in chat + show_help = true, -- Shows help message as virtual lines when waiting for user input + highlight_selection = true, -- Highlight selection + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) + + chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) + auto_follow_cursor = true, -- Auto-follow cursor in chat + auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + insert_at_end = false, -- Move cursor to end of buffer when inserting text + clear_chat_on_new_prompt = false, -- Clears chat on every new prompt + -- default contexts contexts = { buffer = { diff --git a/lua/CopilotChat/actions.lua b/lua/CopilotChat/actions.lua index a682e5ae..fa93976f 100644 --- a/lua/CopilotChat/actions.lua +++ b/lua/CopilotChat/actions.lua @@ -13,7 +13,7 @@ function M.help_actions() end --- User prompt actions ----@param config CopilotChat.config?: The chat configuration +---@param config CopilotChat.config.shared?: The chat configuration ---@return CopilotChat.integrations.actions?: The prompt actions function M.prompt_actions(config) local actions = {} diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index ca1d46ce..3e703702 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -74,18 +74,7 @@ local utils = require('CopilotChat.utils') ---@field agent string? ---@field context string|table|nil ---@field temperature number? ----@field question_header string? ----@field answer_header string? ----@field error_header string? ----@field separator string? ----@field show_folds boolean? ----@field show_help boolean? ----@field auto_follow_cursor boolean? ----@field auto_insert_mode boolean? ----@field clear_chat_on_new_prompt boolean? ----@field highlight_selection boolean? ----@field highlight_headers boolean? ----@field no_chat boolean? +---@field headless boolean? ---@field callback fun(response: string, source: CopilotChat.config.source)? ---@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.config.selection? ---@field window CopilotChat.config.window? @@ -96,16 +85,26 @@ local utils = require('CopilotChat.utils') ---@field log_level string? ---@field proxy string? ---@field allow_insecure boolean? ----@field chat_autocomplete boolean? ---@field history_path string? +---@field question_header string? +---@field answer_header string? +---@field error_header string? +---@field separator string? +---@field show_folds boolean? +---@field show_help boolean? +---@field highlight_selection boolean? +---@field highlight_headers boolean? +---@field chat_autocomplete boolean? +---@field auto_follow_cursor boolean? +---@field auto_insert_mode boolean? +---@field insert_at_end boolean? +---@field clear_chat_on_new_prompt boolean? ---@field contexts table? ---@field prompts table? ---@field mappings CopilotChat.config.mappings? return { - debug = false, -- Enable debug logging (same as 'log_level = 'debug') - log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' - proxy = nil, -- [protocol://]host[:port] Use this proxy - allow_insecure = false, -- Allow insecure server connections + + -- Shared config starts here (can be passed to functions at runtime) system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). @@ -113,23 +112,7 @@ return { context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature - question_header = '## User ', -- Header to use for user questions - answer_header = '## Copilot ', -- Header to use for AI answers - error_header = '## Error ', -- Header to use for errors - separator = '───', -- Separator to use in chat - - chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) - show_folds = true, -- Shows folds for sections in chat - show_help = true, -- Shows help message as virtual lines when waiting for user input - auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt - insert_at_end = false, -- Move cursor to end of buffer when inserting text - clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) - - history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - no_chat = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) + headless = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) callback = nil, -- Callback to use when ask response is received -- default selection @@ -152,6 +135,30 @@ return { zindex = 1, -- determines if window is on top or below other floating windows }, + -- Static config starts here (can be configured only via setup function) + + debug = false, -- Enable debug logging (same as 'log_level = 'debug') + log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' + proxy = nil, -- [protocol://]host[:port] Use this proxy + allow_insecure = false, -- Allow insecure server connections + history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history + + question_header = '## User ', -- Header to use for user questions + answer_header = '## Copilot ', -- Header to use for AI answers + error_header = '## Error ', -- Header to use for errors + separator = '───', -- Separator to use in chat + + show_folds = true, -- Shows folds for sections in chat + show_help = true, -- Shows help message as virtual lines when waiting for user input + highlight_selection = true, -- Highlight selection + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) + + chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) + auto_follow_cursor = true, -- Auto-follow cursor in chat + auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + insert_at_end = false, -- Move cursor to end of buffer when inserting text + clear_chat_on_new_prompt = false, -- Clears chat on every new prompt + -- default contexts contexts = { buffer = { diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index c1938fa0..54d6c6da 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -18,7 +18,6 @@ local WORD = '([^%s]+)' --- @class CopilotChat.state --- @field copilot CopilotChat.Copilot? --- @field source CopilotChat.config.source? ---- @field config CopilotChat.config? --- @field last_prompt string? --- @field last_response string? --- @field chat CopilotChat.ui.Chat? @@ -30,7 +29,6 @@ local state = { -- Current state tracking source = nil, - config = nil, -- Last state tracking last_prompt = nil, @@ -43,7 +41,7 @@ local state = { debug = nil, } ----@param config CopilotChat.config +---@param config CopilotChat.config.shared ---@return CopilotChat.config.selection? local function get_selection(config) local bufnr = state.source and state.source.bufnr @@ -64,7 +62,7 @@ end --- Highlights the selection in the source buffer. ---@param clear boolean ----@param config CopilotChat.config +---@param config CopilotChat.config.shared local function highlight_selection(clear, config) local selection_ns = vim.api.nvim_create_namespace('copilot-chat-selection') for _, buf in ipairs(vim.api.nvim_list_bufs()) do @@ -93,7 +91,7 @@ local function highlight_selection(clear, config) end --- Updates the selection based on previous window ----@param config CopilotChat.config +---@param config CopilotChat.config.shared local function update_selection(config) local prev_winnr = vim.fn.win_getid(vim.fn.winnr('#')) if prev_winnr ~= state.chat.winnr and vim.fn.win_gettype(prev_winnr) == '' then @@ -106,7 +104,7 @@ local function update_selection(config) highlight_selection(false, config) end ----@param config CopilotChat.config +---@param config CopilotChat.config.shared ---@return CopilotChat.ui.Diff.Diff? local function get_diff(config) local block = state.chat:get_closest_block() @@ -171,7 +169,7 @@ end ---@param bufnr number ---@param start_line number ---@param end_line number ----@param config CopilotChat.config +---@param config CopilotChat.config.shared local function jump_to_diff(winnr, bufnr, start_line, end_line, config) vim.api.nvim_win_set_cursor(winnr, { start_line, 0 }) pcall(vim.api.nvim_buf_set_mark, bufnr, '<', start_line, 0, {}) @@ -182,7 +180,7 @@ local function jump_to_diff(winnr, bufnr, start_line, end_line, config) end ---@param diff CopilotChat.ui.Diff.Diff? ----@param config CopilotChat.config +---@param config CopilotChat.config.shared local function apply_diff(diff, config) if not diff or not diff.bufnr then return @@ -199,7 +197,7 @@ local function apply_diff(diff, config) end ---@param prompt string ----@param system_prompt string +---@param system_prompt string? ---@return string, string local function resolve_prompts(prompt, system_prompt) local prompts_to_use = M.prompts() @@ -232,7 +230,7 @@ local function resolve_prompts(prompt, system_prompt) end ---@param prompt string ----@param config CopilotChat.config +---@param config CopilotChat.config.shared ---@return table, string local function resolve_embeddings(prompt, config) local embeddings = utils.ordered_map() @@ -241,8 +239,9 @@ local function resolve_embeddings(prompt, config) local split = vim.split(prompt_context, ':') local context_name = table.remove(split, 1) local context_input = vim.trim(table.concat(split, ':')) - local context_value = config.contexts[context_name] + local context_value = M.config.contexts[context_name] if context_input == '' then + ---@diagnostic disable-next-line: cast-local-type context_input = nil end @@ -268,6 +267,7 @@ local function resolve_embeddings(prompt, config) if config.context then if type(config.context) == 'table' then + ---@diagnostic disable-next-line: param-type-mismatch for _, config_context in ipairs(config.context) do parse_context(config_context) end @@ -279,18 +279,13 @@ local function resolve_embeddings(prompt, config) return embeddings:values(), prompt end ----@param config CopilotChat.config ---@param start_of_chat boolean? -local function finish(config, start_of_chat) - if config.no_chat then - return - end - +local function finish(start_of_chat) if not start_of_chat then state.chat:append('\n\n') end - state.chat:append(config.question_header .. config.separator .. '\n\n') + state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') if state.last_prompt then local has_sticky = false @@ -306,16 +301,10 @@ local function finish(config, start_of_chat) state.chat:finish() end ----@param config CopilotChat.config ---@param err string|table|nil ---@param append_newline boolean? -local function show_error(config, err, append_newline) +local function show_error(err, append_newline) err = err or 'Unknown error' - log.error(vim.inspect(err)) - - if config.no_chat then - return - end if type(err) == 'string' then local message = err:match('^[^:]+:[^:]+:(.+)') or err @@ -329,8 +318,8 @@ local function show_error(config, err, append_newline) state.chat:append('\n') end - state.chat:append(config.error_header .. '\n```error\n' .. err .. '\n```') - finish(config) + state.chat:append(M.config.error_header .. '\n```error\n' .. err .. '\n```') + finish() end --- Map a key to a function. @@ -560,7 +549,7 @@ function M.prompts() end --- Open the chat window. ----@param config CopilotChat.config|CopilotChat.config.prompt|nil +---@param config CopilotChat.config.shared? function M.open(config) -- If we are already in chat window, do nothing if state.chat:active() then @@ -568,7 +557,6 @@ function M.open(config) end config = vim.tbl_deep_extend('force', M.config, config or {}) - state.config = config utils.return_to_normal_mode() state.chat:open(config) state.chat:follow() @@ -581,7 +569,7 @@ function M.close() end --- Toggle the chat window. ----@param config CopilotChat.config|nil +---@param config CopilotChat.config.shared? function M.toggle(config) if state.chat:visible() then M.close() @@ -595,7 +583,7 @@ function M.response() return state.last_response end ---- Select a Copilot GPT model. +--- Select default Copilot GPT model. function M.select_model() async.run(function() local models = vim.tbl_keys(state.copilot:list_models()) @@ -619,7 +607,7 @@ function M.select_model() end) end ---- Select a Copilot agent. +--- Select default Copilot agent. function M.select_agent() async.run(function() local agents = vim.tbl_keys(state.copilot:list_agents()) @@ -645,12 +633,12 @@ end --- Ask a question to the Copilot model. ---@param prompt string? ----@param config CopilotChat.config|CopilotChat.config.prompt|nil +---@param config CopilotChat.config.shared? function M.ask(prompt, config) config = vim.tbl_deep_extend('force', M.config, config or {}) vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot_diagnostics')) - if not config.no_chat then + if not config.headless then M.open(config) end @@ -659,11 +647,11 @@ function M.ask(prompt, config) return end - if not config.no_chat then + if not config.headless then if config.clear_chat_on_new_prompt then - M.stop(true, config) + M.stop(true) elseif state.copilot:stop() then - finish(config, nil) + finish() end state.last_prompt = prompt @@ -717,7 +705,10 @@ function M.ask(prompt, config) if not query_ok then vim.schedule(function() - show_error(config, filtered_embeddings, has_output) + log.error(vim.inspect(filtered_embeddings)) + if not config.headless then + show_error(filtered_embeddings, has_output) + end end) return end @@ -730,13 +721,12 @@ function M.ask(prompt, config) model = selected_model, agent = selected_agent, temperature = config.temperature, - no_history = config.no_chat, + no_history = config.headless, on_progress = function(token) vim.schedule(function() - if not config.no_chat then + if not config.headless then state.chat:append(token) end - has_output = true end) end, @@ -744,7 +734,10 @@ function M.ask(prompt, config) if not ask_ok then vim.schedule(function() - show_error(config, response, has_output) + log.error(vim.inspect(response)) + if not config.headless then + show_error(response, has_output) + end end) return end @@ -753,14 +746,16 @@ function M.ask(prompt, config) return end - if not config.no_chat then + if not config.headless then state.last_response = response state.chat.token_count = token_count state.chat.token_max_count = token_max_count end vim.schedule(function() - finish(config) + if not config.headless then + finish() + end if config.callback then config.callback(response, state.source) end @@ -770,10 +765,7 @@ end --- Stop current copilot output and optionally reset the chat ten show the help message. ---@param reset boolean? ----@param config CopilotChat.config? -function M.stop(reset, config) - config = vim.tbl_deep_extend('force', M.config, config or {}) - +function M.stop(reset) if reset then state.copilot:reset() state.chat:clear() @@ -783,13 +775,12 @@ function M.stop(reset, config) state.copilot:stop() end - finish(config, reset) + finish(reset) end --- Reset the chat window and show the help message. ----@param config CopilotChat.config? -function M.reset(config) - M.stop(true, config) +function M.reset() + M.stop(true) end --- Save the chat history to a file. @@ -840,8 +831,7 @@ function M.load(name, history_path) end end - finish(M.config, #history == 0) - M.open() + finish(#history == 0) end --- Set the log level @@ -878,9 +868,9 @@ function M.setup(config) end end - if config.yank_diff_register then + if config['yank_diff_register'] then utils.deprecate('config.yank_diff_register', 'config.mappings.yank_diff.register') - config.mappings.yank_diff.register = config.yank_diff_register + config.mappings.yank_diff.register = config['yank_diff_register'] end end @@ -949,7 +939,7 @@ function M.setup(config) end) map_key(M.config.mappings.accept_diff, bufnr, function() - apply_diff(state.diff:get_diff(), state.config) + apply_diff(state.diff:get_diff(), state.chat.config) end) end) @@ -1048,7 +1038,7 @@ function M.setup(config) end) map_key(M.config.mappings.accept_diff, bufnr, function() - apply_diff(get_diff(state.config), state.config) + apply_diff(get_diff(state.chat.config), state.chat.config) end) map_key(M.config.mappings.jump_to_diff, bufnr, function() @@ -1060,7 +1050,7 @@ function M.setup(config) return end - local diff = get_diff(state.config) + local diff = get_diff(state.chat.config) if not diff then return end @@ -1085,11 +1075,17 @@ function M.setup(config) end vim.api.nvim_win_set_buf(state.source.winnr, diff_bufnr) - jump_to_diff(state.source.winnr, diff_bufnr, diff.start_line, diff.end_line, state.config) + jump_to_diff( + state.source.winnr, + diff_bufnr, + diff.start_line, + diff.end_line, + state.chat.config + ) end) map_key(M.config.mappings.quickfix_diffs, bufnr, function() - local selection = get_selection(state.config) + local selection = get_selection(state.chat.config) local items = {} for _, section in ipairs(state.chat.sections) do @@ -1121,7 +1117,7 @@ function M.setup(config) end) map_key(M.config.mappings.yank_diff, bufnr, function() - local diff = get_diff(state.config) + local diff = get_diff(state.chat.config) if not diff then return end @@ -1130,7 +1126,7 @@ function M.setup(config) end) map_key(M.config.mappings.show_diff, bufnr, function() - local diff = get_diff(state.config) + local diff = get_diff(state.chat.config) if not diff then return end @@ -1140,7 +1136,7 @@ function M.setup(config) map_key(M.config.mappings.show_system_prompt, bufnr, function() local section = state.chat:get_closest_section() - local system_prompt = state.config.system_prompt + local system_prompt = state.chat.config.system_prompt if section and not section.answer then _, system_prompt = resolve_prompts(section.content, system_prompt) end @@ -1152,7 +1148,7 @@ function M.setup(config) end) map_key(M.config.mappings.show_user_selection, bufnr, function() - local selection = get_selection(state.config) + local selection = get_selection(state.chat.config) if not selection then return end @@ -1164,7 +1160,7 @@ function M.setup(config) local section = state.chat:get_closest_section() local embeddings = {} if section and not section.answer then - embeddings = resolve_embeddings(section.content, state.config) + embeddings = resolve_embeddings(section.content, state.chat.config) end local text = '' @@ -1189,9 +1185,9 @@ function M.setup(config) local is_enter = ev.event == 'BufEnter' if is_enter then - update_selection(state.config) + update_selection(state.chat.config) else - highlight_selection(true, state.config) + highlight_selection(true, state.chat.config) end end, }) @@ -1221,9 +1217,26 @@ function M.setup(config) end end, }) + + -- Add popup and noinsert completeopt if not present + if vim.fn.has('nvim-0.11.0') == 1 then + local completeopt = vim.opt.completeopt:get() + local updated = false + if not vim.tbl_contains(completeopt, 'noinsert') then + updated = true + table.insert(completeopt, 'noinsert') + end + if not vim.tbl_contains(completeopt, 'popup') then + updated = true + table.insert(completeopt, 'popup') + end + if updated then + vim.bo[bufnr].completeopt = table.concat(completeopt, ',') + end + end end - finish(M.config, true) + finish(true) end ) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index ced298da..16d1a4a1 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -61,15 +61,9 @@ end ---@field winnr number? ---@field spinner CopilotChat.ui.Spinner ---@field sections table +---@field config CopilotChat.config ---@field token_count number? ---@field token_max_count number? ----@field layout string? ----@field auto_insert boolean ----@field auto_follow_cursor boolean ----@field highlight_headers boolean ----@field question_header string? ----@field answer_header string? ----@field separator string? local Chat = class(function(self, help, on_buf_create) Overlay.init(self, 'copilot-chat', help, on_buf_create) vim.treesitter.language.register('markdown', self.name) @@ -80,17 +74,9 @@ local Chat = class(function(self, help, on_buf_create) self.sections = {} -- Variables + self.config = {} self.token_count = nil self.token_max_count = nil - - -- Config - self.layout = nil - self.auto_insert = false - self.auto_follow_cursor = true - self.highlight_headers = true - self.question_header = nil - self.answer_header = nil - self.separator = nil end, Overlay) ---@return number @@ -148,7 +134,7 @@ function Chat:render() for l, line in ipairs(lines) do local separator_found = false - if line == self.answer_header .. self.separator then + if line == self.config.answer_header .. self.config.separator then separator_found = true if current_section then current_section.end_line = l - 1 @@ -159,7 +145,7 @@ function Chat:render() start_line = l + 1, blocks = {}, } - elseif line == self.question_header .. self.separator then + elseif line == self.config.question_header .. self.config.separator then separator_found = true if current_section then current_section.end_line = l - 1 @@ -178,12 +164,14 @@ function Chat:render() end -- Highlight separators - if self.highlight_headers and separator_found then - local sep = vim.fn.strwidth(line) - vim.fn.strwidth(self.separator) + if self.config.highlight_headers and separator_found then + local sep = vim.fn.strwidth(line) - vim.fn.strwidth(self.config.separator) -- separator line vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, sep, { virt_text_win_col = sep, - virt_text = { { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' } }, + virt_text = { + { string.rep(self.config.separator, vim.go.columns), 'CopilotChatSeparator' }, + }, priority = 100, strict = false, }) @@ -365,8 +353,8 @@ function Chat:append(str) end -- Decide if we should follow cursor after appending text. - local should_follow_cursor = self.auto_follow_cursor - if self.auto_follow_cursor and self:visible() then + local should_follow_cursor = self.config.auto_follow_cursor + if should_follow_cursor and self:visible() then local current_pos = vim.api.nvim_win_get_cursor(self.winnr) local line_count = vim.api.nvim_buf_line_count(self.bufnr) -- Follow only if the cursor is currently at the last line. @@ -402,13 +390,14 @@ end ---@param config CopilotChat.config function Chat:open(config) self:validate() + self.config = config - local window = config.window + local window = config.window or {} local layout = window.layout local width = window.width > 1 and window.width or math.floor(vim.o.columns * window.width) local height = window.height > 1 and window.height or math.floor(vim.o.lines * window.height) - if self.layout ~= layout then + if self.config.window.layout ~= layout then self:close() end @@ -457,31 +446,6 @@ function Chat:open(config) vim.api.nvim_win_set_buf(self.winnr, self.bufnr) end - self.layout = layout - self.auto_insert = config.auto_insert_mode - self.auto_follow_cursor = config.auto_follow_cursor - self.highlight_headers = config.highlight_headers - self.question_header = config.question_header - self.answer_header = config.answer_header - self.separator = config.separator - - if config.chat_autocomplete and vim.fn.has('nvim-0.11.0') == 1 then - -- Add popup and noinsert if not present - local completeopt = vim.opt.completeopt:get() - local updated = false - if not vim.tbl_contains(completeopt, 'noinsert') then - updated = true - table.insert(completeopt, 'noinsert') - end - if not vim.tbl_contains(completeopt, 'popup') then - updated = true - table.insert(completeopt, 'popup') - end - if updated then - vim.bo[self.bufnr].completeopt = table.concat(completeopt, ',') - end - end - vim.wo[self.winnr].wrap = true vim.wo[self.winnr].linebreak = true vim.wo[self.winnr].cursorline = true @@ -508,7 +472,7 @@ function Chat:close(bufnr) utils.return_to_normal_mode() end - if self.layout == 'replace' then + if self.config.window.layout == 'replace' then if bufnr then self:restore(self.winnr, bufnr) end @@ -525,7 +489,7 @@ function Chat:focus() end vim.api.nvim_set_current_win(self.winnr) - if self.auto_insert and self:active() and vim.bo[self.bufnr].modifiable then + if self.config.auto_insert_mode and self:active() and vim.bo[self.bufnr].modifiable then vim.cmd('startinsert') end end @@ -550,7 +514,7 @@ function Chat:finish() self.spinner:finish() vim.bo[self.bufnr].modifiable = true - if self.auto_insert and self:active() then + if self.config.auto_insert_mode and self:active() then vim.cmd('startinsert') end end From ef5123ee6faaf5d064580b3d7d7654394a66cb45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 24 Nov 2024 13:50:07 +0000 Subject: [PATCH 264/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 50 +++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e1f47654..1612131d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 23 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 24 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -471,10 +471,8 @@ Also see here : >lua { - debug = false, -- Enable debug logging (same as 'log_level = 'debug') - log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' - proxy = nil, -- [protocol://]host[:port] Use this proxy - allow_insecure = false, -- Allow insecure server connections + + -- Shared config starts here (can be passed to functions at runtime) system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). @@ -482,23 +480,7 @@ Also see here : context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). temperature = 0.1, -- GPT result temperature - question_header = '## User ', -- Header to use for user questions - answer_header = '## Copilot ', -- Header to use for AI answers - error_header = '## Error ', -- Header to use for errors - separator = '───', -- Separator to use in chat - - chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) - show_folds = true, -- Shows folds for sections in chat - show_help = true, -- Shows help message as virtual lines when waiting for user input - auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt - insert_at_end = false, -- Move cursor to end of buffer when inserting text - clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) - - history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - no_chat = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) + headless = false, -- Do not write to chat buffer and use history(useful for using callback for custom processing) callback = nil, -- Callback to use when ask response is received -- default selection @@ -521,6 +503,30 @@ Also see here : zindex = 1, -- determines if window is on top or below other floating windows }, + -- Static config starts here (can be configured only via setup function) + + debug = false, -- Enable debug logging (same as 'log_level = 'debug') + log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' + proxy = nil, -- [protocol://]host[:port] Use this proxy + allow_insecure = false, -- Allow insecure server connections + history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history + + question_header = '## User ', -- Header to use for user questions + answer_header = '## Copilot ', -- Header to use for AI answers + error_header = '## Error ', -- Header to use for errors + separator = '───', -- Separator to use in chat + + show_folds = true, -- Shows folds for sections in chat + show_help = true, -- Shows help message as virtual lines when waiting for user input + highlight_selection = true, -- Highlight selection + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) + + chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) + auto_follow_cursor = true, -- Auto-follow cursor in chat + auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + insert_at_end = false, -- Move cursor to end of buffer when inserting text + clear_chat_on_new_prompt = false, -- Clears chat on every new prompt + -- default contexts contexts = { buffer = { From 1dfc47e1faffd7b4f77509f6eb9a86e33a48aecf Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 24 Nov 2024 15:04:15 +0100 Subject: [PATCH 265/338] refactor(config): reorganize shared/static config fields - Moved UI-related config fields to shared section - Updated type definitions and documentation - Fixed selection highlight to respect config - Improved token count display formatting Signed-off-by: Tomas Slusny --- README.md | 24 ++++++++++----------- lua/CopilotChat/config.lua | 42 ++++++++++++++++++------------------- lua/CopilotChat/init.lua | 7 +++++-- lua/CopilotChat/ui/chat.lua | 31 ++++++++++++++++++--------- 4 files changed, 59 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 1432584c..500b6943 100644 --- a/README.md +++ b/README.md @@ -421,7 +421,7 @@ Also see [here](/lua/CopilotChat/config.lua): ```lua { - -- Shared config starts here (can be passed to functions at runtime) + -- Shared config starts here (can be passed to functions at runtime and configured via setup function) system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). @@ -452,12 +452,23 @@ Also see [here](/lua/CopilotChat/config.lua): zindex = 1, -- determines if window is on top or below other floating windows }, + show_help = true, -- Shows help message as virtual lines when waiting for user input + show_folds = true, -- Shows folds for sections in chat + highlight_selection = true, -- Highlight selection + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) + auto_follow_cursor = true, -- Auto-follow cursor in chat + auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + insert_at_end = false, -- Move cursor to end of buffer when inserting text + clear_chat_on_new_prompt = false, -- Clears chat on every new prompt + -- Static config starts here (can be configured only via setup function) debug = false, -- Enable debug logging (same as 'log_level = 'debug') log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections + + chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history question_header = '## User ', -- Header to use for user questions @@ -465,17 +476,6 @@ Also see [here](/lua/CopilotChat/config.lua): error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat - show_folds = true, -- Shows folds for sections in chat - show_help = true, -- Shows help message as virtual lines when waiting for user input - highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) - - chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) - auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt - insert_at_end = false, -- Move cursor to end of buffer when inserting text - clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - -- default contexts contexts = { buffer = { diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 3e703702..7f2cd97f 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -78,6 +78,14 @@ local utils = require('CopilotChat.utils') ---@field callback fun(response: string, source: CopilotChat.config.source)? ---@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.config.selection? ---@field window CopilotChat.config.window? +---@field show_help boolean? +---@field show_folds boolean? +---@field highlight_selection boolean? +---@field highlight_headers boolean? +---@field auto_follow_cursor boolean? +---@field auto_insert_mode boolean? +---@field insert_at_end boolean? +---@field clear_chat_on_new_prompt boolean? --- CopilotChat default configuration ---@class CopilotChat.config : CopilotChat.config.shared @@ -85,26 +93,18 @@ local utils = require('CopilotChat.utils') ---@field log_level string? ---@field proxy string? ---@field allow_insecure boolean? +---@field chat_autocomplete boolean? ---@field history_path string? ---@field question_header string? ---@field answer_header string? ---@field error_header string? ---@field separator string? ----@field show_folds boolean? ----@field show_help boolean? ----@field highlight_selection boolean? ----@field highlight_headers boolean? ----@field chat_autocomplete boolean? ----@field auto_follow_cursor boolean? ----@field auto_insert_mode boolean? ----@field insert_at_end boolean? ----@field clear_chat_on_new_prompt boolean? ---@field contexts table? ---@field prompts table? ---@field mappings CopilotChat.config.mappings? return { - -- Shared config starts here (can be passed to functions at runtime) + -- Shared config starts here (can be passed to functions at runtime and configured via setup function) system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). @@ -135,12 +135,23 @@ return { zindex = 1, -- determines if window is on top or below other floating windows }, + show_help = true, -- Shows help message as virtual lines when waiting for user input + show_folds = true, -- Shows folds for sections in chat + highlight_selection = true, -- Highlight selection + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) + auto_follow_cursor = true, -- Auto-follow cursor in chat + auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + insert_at_end = false, -- Move cursor to end of buffer when inserting text + clear_chat_on_new_prompt = false, -- Clears chat on every new prompt + -- Static config starts here (can be configured only via setup function) debug = false, -- Enable debug logging (same as 'log_level = 'debug') log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections + + chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history question_header = '## User ', -- Header to use for user questions @@ -148,17 +159,6 @@ return { error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat - show_folds = true, -- Shows folds for sections in chat - show_help = true, -- Shows help message as virtual lines when waiting for user input - highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) - - chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) - auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt - insert_at_end = false, -- Move cursor to end of buffer when inserting text - clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - -- default contexts contexts = { buffer = { diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 54d6c6da..be612fe0 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -69,7 +69,7 @@ local function highlight_selection(clear, config) vim.api.nvim_buf_clear_namespace(buf, selection_ns, 0, -1) end - if clear then + if clear or not config.highlight_selection then return end @@ -948,7 +948,10 @@ function M.setup(config) state.chat:delete() end state.chat = Chat( - M.config.show_help and key_to_info('show_help', M.config.mappings.show_help), + M.config.question_header, + M.config.answer_header, + M.config.separator, + key_to_info('show_help', M.config.mappings.show_help), function(bufnr) map_key(M.config.mappings.show_help, bufnr, function() local chat_help = '**`Special tokens`**\n' diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 16d1a4a1..6e10b014 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -57,17 +57,24 @@ end ---@field content string? ---@class CopilotChat.ui.Chat : CopilotChat.ui.Overlay +---@field question_header string +---@field answer_header string +---@field separator string ---@field header_ns number ---@field winnr number? ---@field spinner CopilotChat.ui.Spinner ---@field sections table ----@field config CopilotChat.config +---@field config CopilotChat.config.shared ---@field token_count number? ---@field token_max_count number? -local Chat = class(function(self, help, on_buf_create) +local Chat = class(function(self, question_header, answer_header, separator, help, on_buf_create) Overlay.init(self, 'copilot-chat', help, on_buf_create) vim.treesitter.language.register('markdown', self.name) + self.question_header = question_header + self.answer_header = answer_header + self.separator = separator + self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') self.winnr = nil self.spinner = nil @@ -134,7 +141,7 @@ function Chat:render() for l, line in ipairs(lines) do local separator_found = false - if line == self.config.answer_header .. self.config.separator then + if line == self.answer_header .. self.separator then separator_found = true if current_section then current_section.end_line = l - 1 @@ -145,7 +152,7 @@ function Chat:render() start_line = l + 1, blocks = {}, } - elseif line == self.config.question_header .. self.config.separator then + elseif line == self.question_header .. self.separator then separator_found = true if current_section then current_section.end_line = l - 1 @@ -165,12 +172,12 @@ function Chat:render() -- Highlight separators if self.config.highlight_headers and separator_found then - local sep = vim.fn.strwidth(line) - vim.fn.strwidth(self.config.separator) + local sep = vim.fn.strwidth(line) - vim.fn.strwidth(self.separator) -- separator line vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, sep, { virt_text_win_col = sep, virt_text = { - { string.rep(self.config.separator, vim.go.columns), 'CopilotChatSeparator' }, + { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' }, }, priority = 100, strict = false, @@ -213,10 +220,14 @@ function Chat:render() local last_section = sections[#sections] if last_section and not last_section.answer then - local msg = self.help + local msg = self.config.show_help and self.help or '' if self.token_count and self.token_max_count then - msg = msg .. '\n' .. self.token_count .. '/' .. self.token_max_count .. ' tokens used' + if msg ~= '' then + msg = msg .. '\n' + end + msg = msg .. self.token_count .. '/' .. self.token_max_count .. ' tokens used' end + self:show_help(msg, last_section.start_line - last_section.end_line - 1) else self:clear_help() @@ -387,7 +398,7 @@ function Chat:clear() vim.bo[self.bufnr].modifiable = false end ----@param config CopilotChat.config +---@param config CopilotChat.config.shared function Chat:open(config) self:validate() self.config = config @@ -454,7 +465,7 @@ function Chat:open(config) if config.show_folds then vim.wo[self.winnr].foldcolumn = '1' vim.wo[self.winnr].foldmethod = 'expr' - vim.wo[self.winnr].foldexpr = "v:lua.CopilotChatFoldExpr(v:lnum, '" .. config.separator .. "')" + vim.wo[self.winnr].foldexpr = "v:lua.CopilotChatFoldExpr(v:lnum, '" .. self.separator .. "')" else vim.wo[self.winnr].foldcolumn = '0' end From 799fbeacc759f6e7db862fcedf91b8d8ae8a60b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 24 Nov 2024 14:08:49 +0000 Subject: [PATCH 266/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 1612131d..abca7184 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -472,7 +472,7 @@ Also see here : >lua { - -- Shared config starts here (can be passed to functions at runtime) + -- Shared config starts here (can be passed to functions at runtime and configured via setup function) system_prompt = prompts.COPILOT_INSTRUCTIONS, -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4o', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). @@ -503,12 +503,23 @@ Also see here : zindex = 1, -- determines if window is on top or below other floating windows }, + show_help = true, -- Shows help message as virtual lines when waiting for user input + show_folds = true, -- Shows folds for sections in chat + highlight_selection = true, -- Highlight selection + highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) + auto_follow_cursor = true, -- Auto-follow cursor in chat + auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt + insert_at_end = false, -- Move cursor to end of buffer when inserting text + clear_chat_on_new_prompt = false, -- Clears chat on every new prompt + -- Static config starts here (can be configured only via setup function) debug = false, -- Enable debug logging (same as 'log_level = 'debug') log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections + + chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history question_header = '## User ', -- Header to use for user questions @@ -516,17 +527,6 @@ Also see here : error_header = '## Error ', -- Header to use for errors separator = '───', -- Separator to use in chat - show_folds = true, -- Shows folds for sections in chat - show_help = true, -- Shows help message as virtual lines when waiting for user input - highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) - - chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) - auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt - insert_at_end = false, -- Move cursor to end of buffer when inserting text - clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - -- default contexts contexts = { buffer = { From 273f43a6fb0f5f11d6ed9044beae2970b482f6ec Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 24 Nov 2024 18:48:06 +0100 Subject: [PATCH 267/338] fix: use correct function name for deprecation --- lua/CopilotChat/select.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 5eef4ecf..37a72bd7 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -149,12 +149,12 @@ function M.unnamed(source) end function M.clipboard() - utils.deprecated('selection.clipboard', 'context.register:+') + utils.deprecate('selection.clipboard', 'context.register:+') return nil end function M.gitdiff() - utils.deprecated('selection.gitdiff', 'context.gitdiff') + utils.deprecate('selection.gitdiff', 'context.gitdiff') return nil end From ac7edc4f8021124cb29892f1facdb1f3001688b6 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 24 Nov 2024 19:08:09 +0100 Subject: [PATCH 268/338] refactor: move embed truncation logic to proper places This commit reorganizes the code to handle truncation of large files and embeddings in a more consistent way. The changes include: - Moving ordered_map implementation to top of utils.lua - Centralizing truncation logic for embeddings with BIG_EMBED_THRESHOLD - Simplifying outline truncation by moving it to embedding generation - Updating class type definitions to be more consistent - Removing duplicate code related to truncation handlers The main goal is to make the codebase more maintainable by having truncation logic in appropriate locations rather than scattered across different files. Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 14 +----- lua/CopilotChat/copilot.lua | 35 ++++++--------- lua/CopilotChat/ui/overlay.lua | 2 +- lua/CopilotChat/ui/spinner.lua | 2 +- lua/CopilotChat/utils.lua | 78 +++++++++++++++++----------------- 5 files changed, 56 insertions(+), 75 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 63adfac5..fc0a89cc 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -52,7 +52,6 @@ local OFF_SIDE_RULE_LANGUAGES = { 'fsharp', } -local OUTLINE_THRESHOLD = 600 local MULTI_FILE_THRESHOLD = 3 local function spatial_distance_cosine(a, b) @@ -339,19 +338,8 @@ function M.filter_embeddings(copilot, prompt, embeddings) -- Map embeddings by filename for _, embed in ipairs(embeddings) do original_map:set(embed.filename, embed) - if embed.filetype ~= 'raw' then - local outline = M.outline(embed.content, embed.filename, embed.filetype) - local outline_lines = vim.split(outline.content, '\n') - - -- If outline is too big, truncate it - if #outline_lines > 0 and #outline_lines > OUTLINE_THRESHOLD then - outline_lines = vim.list_slice(outline_lines, 1, OUTLINE_THRESHOLD) - table.insert(outline_lines, '... (truncated)') - end - - outline.content = table.concat(outline_lines, '\n') - embedded_map:set(embed.filename, outline) + embedded_map:set(embed.filename, M.outline(embed.content, embed.filename, embed.filetype)) else embedded_map:set(embed.filename, embed) end diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index f7b703d7..a8a2540d 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -29,6 +29,8 @@ local temp_file = utils.temp_file --- Constants local CONTEXT_FORMAT = '[#file:%s](#file:%s-context)' local BIG_FILE_THRESHOLD = 2000 +local BIG_EMBED_THRESHOLD = 600 +local TRUNCATED = '... (truncated)' local TIMEOUT = 30000 local VERSION_HEADERS = { ['editor-version'] = 'Neovim/' @@ -114,12 +116,9 @@ end ---@return string local function generate_line_numbers(content, start_line) local lines = vim.split(content, '\n') - local truncated = false - - -- If the file is too big, truncate it if #lines > BIG_FILE_THRESHOLD then lines = vim.list_slice(lines, 1, BIG_FILE_THRESHOLD) - truncated = true + table.insert(lines, TRUNCATED) end local total_lines = #lines @@ -129,10 +128,6 @@ local function generate_line_numbers(content, start_line) lines[i] = formatted_line_number .. ': ' .. line end - if truncated then - table.insert(lines, '... (truncated)') - end - content = table.concat(lines, '\n') return content end @@ -301,26 +296,24 @@ local function generate_embedding_request(inputs, model) return { dimensions = 512, input = vim.tbl_map(function(input) - local out = '' + local lines = vim.split(input.content, '\n') + if #lines > BIG_EMBED_THRESHOLD then + lines = vim.list_slice(lines, 1, BIG_EMBED_THRESHOLD) + table.insert(lines, TRUNCATED) + end + local content = table.concat(lines, '\n') + if input.filetype == 'raw' then - out = input.content .. '\n' + return content else - out = out - .. string.format( - 'File: `%s`\n```%s\n%s\n```', - input.filename, - input.filetype, - input.content - ) + return string.format('File: `%s`\n```%s\n%s\n```', input.filename, input.filetype, content) end - - return out end, inputs), model = model, } end ----@class CopilotChat.Copilot : CopilotChat.utils.Class +---@class CopilotChat.Copilot : Class ---@field history table ---@field embedding_cache table ---@field policies table @@ -873,7 +866,7 @@ end --- Generate embeddings for the given inputs ---@param inputs table: The inputs to embed ----@param opts CopilotChat.copilot.embed.opts: Options for the request +---@param opts CopilotChat.copilot.embed.opts?: Options for the request ---@return table function Copilot:embed(inputs, opts) if not inputs or #inputs == 0 then diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index 2456d767..b5d15317 100644 --- a/lua/CopilotChat/ui/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -1,7 +1,7 @@ local utils = require('CopilotChat.utils') local class = utils.class ----@class CopilotChat.ui.Overlay : CopilotChat.utils.Class +---@class CopilotChat.ui.Overlay : Class ---@field name string ---@field help string ---@field help_ns number diff --git a/lua/CopilotChat/ui/spinner.lua b/lua/CopilotChat/ui/spinner.lua index 13667cf0..d07e25e3 100644 --- a/lua/CopilotChat/ui/spinner.lua +++ b/lua/CopilotChat/ui/spinner.lua @@ -14,7 +14,7 @@ local spinner_frames = { '⠏', } ----@class CopilotChat.ui.Spinner : CopilotChat.utils.Class +---@class CopilotChat.ui.Spinner : Class ---@field ns number ---@field bufnr number ---@field timer table diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index ef3a02b2..655b979a 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,14 +1,14 @@ local M = {} M.timers = {} ----@class CopilotChat.utils.Class +---@class Class ---@field new fun(...):table ---@field init fun(self, ...) --- Create class ---@param fn function The class constructor ---@param parent table? The parent class ----@return CopilotChat.utils.Class +---@return Class function M.class(fn, parent) local out = {} out.__index = out @@ -38,6 +38,43 @@ function M.class(fn, parent) return out end +---@class OrderedMap +---@field set fun(self:OrderedMap, key:any, value:any) +---@field get fun(self:OrderedMap, key:any):any +---@field keys fun(self:OrderedMap):table +---@field values fun(self:OrderedMap):table + +--- Create an ordered map +---@return OrderedMap +function M.ordered_map() + return { + _keys = {}, + _data = {}, + set = function(self, key, value) + if not self._data[key] then + table.insert(self._keys, key) + end + self._data[key] = value + end, + + get = function(self, key) + return self._data[key] + end, + + keys = function(self) + return self._keys + end, + + values = function(self) + local result = {} + for _, key in ipairs(self._keys) do + table.insert(result, self._data[key]) + end + return result + end, + } +end + --- Check if the current version of neovim is stable ---@return boolean function M.is_stable() @@ -187,41 +224,4 @@ function M.win_cwd(winnr) return dir end ----@class OrderedMap ----@field set fun(self:OrderedMap, key:any, value:any) ----@field get fun(self:OrderedMap, key:any):any ----@field keys fun(self:OrderedMap):table ----@field values fun(self:OrderedMap):table - ---- Create an ordered map ----@return OrderedMap -function M.ordered_map() - return { - _keys = {}, - _data = {}, - set = function(self, key, value) - if not self._data[key] then - table.insert(self._keys, key) - end - self._data[key] = value - end, - - get = function(self, key) - return self._data[key] - end, - - keys = function(self) - return self._keys - end, - - values = function(self) - local result = {} - for _, key in ipairs(self._keys) do - table.insert(result, self._data[key]) - end - return result - end, - } -end - return M From 6066bde47fac7d6379a88d270ef4324e93aaf891 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Nov 2024 09:42:21 +0100 Subject: [PATCH 269/338] feat: symbol-based filtering for context matches Add symbol-based filtering system for workspace context to improve match relevancy. This adds a new ranking system that scores matches based on: - Exact filename matches (highest priority) - Symbol name matches (high priority) - Symbol signature matches (medium priority) The system uses a scoring mechanism that prioritizes exact matches and implements TOP_SYMBOLS (64) and TOP_RELATED (20) thresholds to limit results. This improves context matching accuracy by considering code structure rather than just content similarity. Also adds symbol information to debug output for easier troubleshooting. Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 171 +++++++++++++++++++++++++++-------- lua/CopilotChat/ui/debug.lua | 16 ++++ 2 files changed, 149 insertions(+), 38 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index fc0a89cc..0dbeeef1 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -1,3 +1,15 @@ +---@class CopilotChat.context.symbol +---@field name string? +---@field signature string +---@field type string +---@field start_row number +---@field start_col number +---@field end_row number +---@field end_col number + +---@class CopilotChat.context.outline : CopilotChat.copilot.embed +---@field symbols table + local log = require('plenary.log') local utils = require('CopilotChat.utils') @@ -23,13 +35,14 @@ local OUTLINE_TYPES = { 'import_statement', 'import_from_statement', -- markdown - 'atx_h1_marker', - 'atx_h2_marker', - 'atx_h3_marker', - 'atx_h4_marker', - 'atx_h5_marker', + 'atx_heading', 'list_item', - 'block_quote', +} + +local NAME_TYPES = { + 'name', + 'identifier', + 'heading_content', } local COMMENT_TYPES = { @@ -52,6 +65,8 @@ local OFF_SIDE_RULE_LANGUAGES = { 'fsharp', } +local TOP_SYMBOLS = 64 +local TOP_RELATED = 20 local MULTI_FILE_THRESHOLD = 3 local function spatial_distance_cosine(a, b) @@ -68,6 +83,7 @@ local function spatial_distance_cosine(a, b) return dot_product / (magnitude_a * magnitude_b) end +---@param query string local function data_ranked_by_relatedness(query, data, top_n) local scores = {} for i, item in pairs(data) do @@ -79,27 +95,86 @@ local function data_ranked_by_relatedness(query, data, top_n) local result = {} for i = 1, math.min(top_n, #scores) do local srt = scores[i] - table.insert(result, vim.tbl_extend('keep', data[srt.index], { score = srt.score })) + table.insert(result, vim.tbl_extend('force', data[srt.index], { score = srt.score })) end return result end ---- Build an outline from a string +---@param query string +---@param data table +---@param top_n number +local function data_ranked_by_symbols(query, data, top_n) + local query_terms = {} + for term in query:lower():gmatch('%w+') do + query_terms[term] = true + end + + local results = {} + for _, entry in ipairs(data) do + local score = 0 + local filename = entry.filename and entry.filename:lower() or '' + + -- Filename matches (highest priority) + for term in pairs(query_terms) do + if filename:find(term, 1, true) then + score = score + 15 + if vim.fn.fnamemodify(filename, ':t'):gsub('%..*$', '') == term then + score = score + 10 + end + end + end + + -- Symbol matches + if entry.symbols then + for _, symbol in ipairs(entry.symbols) do + for term in pairs(query_terms) do + -- Check symbol name (high priority) + if symbol.name and symbol.name:lower():find(term, 1, true) then + score = score + 5 + if symbol.name:lower() == term then + score = score + 3 + end + end + + -- Check signature (medium priority) + -- This catches parameter names, return types, etc + if symbol.signature and symbol.signature:lower():find(term, 1, true) then + score = score + 2 + end + end + end + end + + table.insert(results, vim.tbl_extend('force', entry, { score = score })) + end + + table.sort(results, function(a, b) + return a.score > b.score + end) + return vim.list_slice(results, 1, top_n) +end + +--- Build an outline and symbols from a string --- FIXME: Handle multiline function argument definitions when building the outline ---@param content string ---@param name string ---@param ft string? ----@return CopilotChat.copilot.embed +---@return CopilotChat.context.outline function M.outline(content, name, ft) ft = ft or 'text' - local lines = vim.split(content, '\n') - local base_output = { - content = content, + local output = { filename = name, filetype = ft, + content = content, + symbols = {}, } + if ft == 'raw' then + return output + end + + local lines = vim.split(content, '\n') local lang = vim.treesitter.language.get_lang(ft) local ok, parser = false, nil if lang then @@ -109,7 +184,7 @@ function M.outline(content, name, ft) ft = string.gsub(ft, 'react', '') ok, parser = pcall(vim.treesitter.get_string_parser, content, ft) if not ok or not parser then - return base_output + return output end end @@ -118,7 +193,18 @@ function M.outline(content, name, ft) local comment_lines = {} local depth = 0 - local function get_outline_lines(node) + local function get_node_name(node) + for _, name_type in ipairs(NAME_TYPES) do + local name_field = node:field(name_type) + if name_field and #name_field > 0 then + return vim.treesitter.get_node_text(name_field[1], content) + end + end + + return nil + end + + local function parse_node(node) local type = node:type() local parent = node:parent() local is_outline = vim.tbl_contains(OUTLINE_TYPES, type) @@ -131,6 +217,7 @@ function M.outline(content, name, ft) if is_outline then depth = depth + 1 + -- Handle comments if #comment_lines > 0 then for _, line in ipairs(comment_lines) do table.insert(outline_lines, string.rep(' ', depth) .. line) @@ -139,10 +226,20 @@ function M.outline(content, name, ft) end local start_line = lines[start_row + 1] - local signature_start = start_line:sub(start_col + 1) - table.insert(outline_lines, string.rep(' ', depth) .. vim.trim(signature_start)) + local signature_start = vim.trim(start_line:sub(start_col + 1)) + table.insert(outline_lines, string.rep(' ', depth) .. signature_start) + + -- Store symbol information + table.insert(output.symbols, { + name = get_node_name(node), + signature = signature_start, + type = type, + start_row = start_row + 1, + start_col = start_col + 1, + end_row = end_row, + end_col = end_col, + }) - -- If the function definition spans multiple lines, add an ellipsis if start_row ~= end_row then table.insert(outline_lines, string.rep(' ', depth + 1) .. '...') else @@ -160,7 +257,7 @@ function M.outline(content, name, ft) if not skip_inner then for child in node:iter_children() do - get_outline_lines(child) + parse_node(child) end end @@ -174,19 +271,13 @@ function M.outline(content, name, ft) end end - get_outline_lines(root) + parse_node(root) - -- Concatenate the outline lines - local result_content = table.concat(outline_lines, '\n') - if result_content == '' then - return base_output + if #outline_lines > 0 then + output.content = table.concat(outline_lines, '\n') end - return { - content = result_content, - filename = name, - filetype = ft, - } + return output end --- Get list of all files in workspace @@ -338,26 +429,30 @@ function M.filter_embeddings(copilot, prompt, embeddings) -- Map embeddings by filename for _, embed in ipairs(embeddings) do original_map:set(embed.filename, embed) - if embed.filetype ~= 'raw' then - embedded_map:set(embed.filename, M.outline(embed.content, embed.filename, embed.filetype)) - else - embedded_map:set(embed.filename, embed) - end + embedded_map:set(embed.filename, M.outline(embed.content, embed.filename, embed.filetype)) + end + + -- Rank embeddings by symbols + local ranked_data = data_ranked_by_symbols(prompt, embedded_map:values(), TOP_SYMBOLS) + log.debug('Ranked data:', #ranked_data) + for i, item in ipairs(ranked_data) do + log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end -- Get embeddings from all items - local embedded_data = copilot:embed(embedded_map:values()) + local embedded_data = copilot:embed(ranked_data) -- Rate embeddings by relatedness to the query - local ranked_data = data_ranked_by_relatedness(table.remove(embedded_data, 1), embedded_data, 20) - log.debug('Ranked data:', #ranked_data) - for i, item in ipairs(ranked_data) do + local ranked_embeddings = + data_ranked_by_relatedness(table.remove(embedded_data, 1), embedded_data, TOP_RELATED) + log.debug('Ranked embeddings:', #ranked_embeddings) + for i, item in ipairs(ranked_embeddings) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end -- Return original content in ranked order local result = {} - for _, ranked_item in ipairs(ranked_data) do + for _, ranked_item in ipairs(ranked_embeddings) do local original = original_map:get(ranked_item.filename) if original then table.insert(result, original) diff --git a/lua/CopilotChat/ui/debug.lua b/lua/CopilotChat/ui/debug.lua index a09ec7cd..10f6ab30 100644 --- a/lua/CopilotChat/ui/debug.lua +++ b/lua/CopilotChat/ui/debug.lua @@ -26,6 +26,22 @@ local function build_debug_info() local buf = context.buffer(vim.api.nvim_get_current_buf()) local outline = buf and context.outline(buf.content, buf.filename, buf.filetype) if outline then + table.insert(lines, 'Current buffer symbols:') + for _, symbol in ipairs(outline.symbols) do + table.insert( + lines, + string.format( + '%s `%s` (%s %s %s %s) - `%s`', + symbol.type, + symbol.name, + symbol.start_row, + symbol.start_col, + symbol.end_row, + symbol.end_col, + symbol.signature + ) + ) + end table.insert(lines, 'Current buffer outline:') table.insert(lines, '`' .. outline.filename .. '`') table.insert(lines, '```' .. outline.filetype) From 9518cc6292d47a3f1ce55568c1444e6b050657a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 25 Nov 2024 09:35:30 +0000 Subject: [PATCH 270/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index abca7184..ef412d32 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 24 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 25 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From df19e7ab15537436a26f59fa991f2fa81d847a0d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Nov 2024 10:47:25 +0100 Subject: [PATCH 271/338] fix: improve prompt embedding and ranking The changes improve how prompts are embedded and ranked in the context system: - Move prompt embedding to after initial data ranking to maintain correct ordering - Fix log output to show correct embedded query content - Trim whitespace from resolved prompts to improve matching This ensures more accurate context retrieval and better prompt handling in the chat system. --- lua/CopilotChat/context.lua | 18 ++++++++++-------- lua/CopilotChat/init.lua | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 0dbeeef1..2e03d459 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -420,12 +420,6 @@ function M.filter_embeddings(copilot, prompt, embeddings) local original_map = utils.ordered_map() local embedded_map = utils.ordered_map() - embedded_map:set('prompt', { - content = prompt, - filename = 'prompt', - filetype = 'raw', - }) - -- Map embeddings by filename for _, embed in ipairs(embeddings) do original_map:set(embed.filename, embed) @@ -439,12 +433,20 @@ function M.filter_embeddings(copilot, prompt, embeddings) log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end + -- Add prompt so it can be embedded + table.insert(ranked_data, { + content = prompt, + filename = 'prompt', + filetype = 'raw', + }) + -- Get embeddings from all items local embedded_data = copilot:embed(ranked_data) -- Rate embeddings by relatedness to the query - local ranked_embeddings = - data_ranked_by_relatedness(table.remove(embedded_data, 1), embedded_data, TOP_RELATED) + local embedded_query = table.remove(embedded_data, #embedded_data) + log.debug('Embedded query:', embedded_query.content) + local ranked_embeddings = data_ranked_by_relatedness(embedded_query, embedded_data, TOP_RELATED) log.debug('Ranked embeddings:', #ranked_embeddings) for i, item in ipairs(ranked_embeddings) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index be612fe0..7bd61285 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -664,12 +664,12 @@ function M.ask(prompt, config) local resolved_prompt, system_prompt = resolve_prompts(prompt, config.system_prompt) -- Remove sticky prefix - prompt = table.concat( + prompt = vim.trim(table.concat( vim.tbl_map(function(l) return l:gsub('>%s+', '') end, vim.split(resolved_prompt, '\n')), '\n' - ) + )) -- Resolve embeddings local embeddings, embedded_prompt = resolve_embeddings(prompt, config) From ca89c0a0af5283517c6b5953aee63e0f64fe807e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Nov 2024 17:41:28 +0100 Subject: [PATCH 272/338] refactor(context): simplify outline building and add typing The commit improves the outline building functionality by: - Adding field_declaration and record_declaration to outline types - Removing redundant comment handling logic to simplify the code - Adding proper type annotations for functions - Extracting reusable functions for getting node names and signatures - Adding embedding type definition to copilot class --- lua/CopilotChat/context.lua | 121 +++++++++++++++++------------------- lua/CopilotChat/copilot.lua | 1 + 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 2e03d459..15a63450 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -27,14 +27,15 @@ local OUTLINE_TYPES = { 'template_declaration', 'macro_declaration', 'constructor_declaration', + 'field_declaration', 'class_definition', 'class_declaration', 'interface_definition', 'interface_declaration', + 'record_declaration', 'type_alias_declaration', 'import_statement', 'import_from_statement', - -- markdown 'atx_heading', 'list_item', } @@ -45,17 +46,6 @@ local NAME_TYPES = { 'heading_content', } -local COMMENT_TYPES = { - 'comment', - 'line_comment', - 'block_comment', - 'doc_comment', -} - -local IGNORED_TYPES = { - 'export_statement', -} - local OFF_SIDE_RULE_LANGUAGES = { 'python', 'coffeescript', @@ -69,6 +59,10 @@ local TOP_SYMBOLS = 64 local TOP_RELATED = 20 local MULTI_FILE_THRESHOLD = 3 +--- Compute the cosine similarity between two vectors +---@param a table +---@param b table +---@return number local function spatial_distance_cosine(a, b) local dot_product = 0 local magnitude_a = 0 @@ -83,7 +77,11 @@ local function spatial_distance_cosine(a, b) return dot_product / (magnitude_a * magnitude_b) end ----@param query string +--- Rank data by relatedness to the query +---@param query CopilotChat.copilot.embed +---@param data table +---@param top_n number +---@return table local function data_ranked_by_relatedness(query, data, top_n) local scores = {} for i, item in pairs(data) do @@ -100,6 +98,7 @@ local function data_ranked_by_relatedness(query, data, top_n) return result end +--- Rank data by symbols ---@param query string ---@param data table ---@param top_n number @@ -154,17 +153,51 @@ local function data_ranked_by_symbols(query, data, top_n) return vim.list_slice(results, 1, top_n) end +--- Get the full signature of a declaration +---@param start_row number +---@param start_col number +---@param lines table +---@return string +local function get_full_signature(start_row, start_col, lines) + local start_line = lines[start_row + 1] + local signature = vim.trim(start_line:sub(start_col + 1)) + + -- Look ahead for opening brace on next line + if not signature:match('{') and (start_row + 2) <= #lines then + local next_line = vim.trim(lines[start_row + 2]) + if next_line:match('^{') then + signature = signature .. ' {' + end + end + + return signature +end + +--- Get the name of a node +---@param node table +---@param content string +---@return string? +local function get_node_name(node, content) + for _, name_type in ipairs(NAME_TYPES) do + local name_field = node:field(name_type) + if name_field and #name_field > 0 then + return vim.treesitter.get_node_text(name_field[1], content) + end + end + + return nil +end + --- Build an outline and symbols from a string ---- FIXME: Handle multiline function argument definitions when building the outline ---@param content string ----@param name string +---@param filename string ---@param ft string? ---@return CopilotChat.context.outline -function M.outline(content, name, ft) +function M.outline(content, filename, ft) ft = ft or 'text' local output = { - filename = name, + filename = filename, filetype = ft, content = content, symbols = {}, @@ -190,48 +223,22 @@ function M.outline(content, name, ft) local root = parser:parse()[1]:root() local outline_lines = {} - local comment_lines = {} local depth = 0 - local function get_node_name(node) - for _, name_type in ipairs(NAME_TYPES) do - local name_field = node:field(name_type) - if name_field and #name_field > 0 then - return vim.treesitter.get_node_text(name_field[1], content) - end - end - - return nil - end - local function parse_node(node) local type = node:type() - local parent = node:parent() local is_outline = vim.tbl_contains(OUTLINE_TYPES, type) - local is_comment = vim.tbl_contains(COMMENT_TYPES, type) - local is_ignored = vim.tbl_contains(IGNORED_TYPES, type) - or parent and vim.tbl_contains(IGNORED_TYPES, parent:type()) local start_row, start_col, end_row, end_col = node:range() - local skip_inner = false if is_outline then depth = depth + 1 - - -- Handle comments - if #comment_lines > 0 then - for _, line in ipairs(comment_lines) do - table.insert(outline_lines, string.rep(' ', depth) .. line) - end - comment_lines = {} - end - - local start_line = lines[start_row + 1] - local signature_start = vim.trim(start_line:sub(start_col + 1)) + local name = get_node_name(node, content) + local signature_start = get_full_signature(start_row, start_col, lines) table.insert(outline_lines, string.rep(' ', depth) .. signature_start) -- Store symbol information table.insert(output.symbols, { - name = get_node_name(node), + name = name, signature = signature_start, type = type, start_row = start_row + 1, @@ -239,30 +246,14 @@ function M.outline(content, name, ft) end_row = end_row, end_col = end_col, }) - - if start_row ~= end_row then - table.insert(outline_lines, string.rep(' ', depth + 1) .. '...') - else - skip_inner = true - end - elseif is_comment then - skip_inner = true - local comment = vim.split(vim.treesitter.get_node_text(node, content), '\n') - for _, line in ipairs(comment) do - table.insert(comment_lines, vim.trim(line)) - end - elseif not is_ignored then - comment_lines = {} end - if not skip_inner then - for child in node:iter_children() do - parse_node(child) - end + for child in node:iter_children() do + parse_node(child) end if is_outline then - if not skip_inner and not vim.tbl_contains(OFF_SIDE_RULE_LANGUAGES, ft) then + if not vim.tbl_contains(OFF_SIDE_RULE_LANGUAGES, ft) then local end_line = lines[end_row + 1] local signature_end = vim.trim(end_line:sub(1, end_col)) table.insert(outline_lines, string.rep(' ', depth) .. signature_end) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index a8a2540d..d880ae32 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -2,6 +2,7 @@ ---@field content string ---@field filename string ---@field filetype string +---@field embedding table ---@class CopilotChat.copilot.ask.opts ---@field selection CopilotChat.config.selection? From c1c7630364f21bb048b675b95ce34fc9d8dd72e3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Nov 2024 18:38:42 +0100 Subject: [PATCH 273/338] refactor: simplify context filtering and ranking Simplify context filtering and ranking by removing redundant data structures and utilizing more of vim's built-in table functions. This change: - Removes OrderedMap usage in favor of direct table operations - Combines mapping and scoring operations - Preserves original content in outline objects - Uses vim.tbl_map and vim.tbl_extend for cleaner data transformations - Improves code readability and reduces complexity The functionality remains the same while making the code more maintainable. --- lua/CopilotChat/context.lua | 70 ++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 15a63450..d3352746 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -83,19 +83,19 @@ end ---@param top_n number ---@return table local function data_ranked_by_relatedness(query, data, top_n) - local scores = {} - for i, item in pairs(data) do - scores[i] = { index = i, score = spatial_distance_cosine(item.embedding, query.embedding) } - end - table.sort(scores, function(a, b) + data = vim.tbl_map(function(item) + return vim.tbl_extend( + 'force', + item, + { score = spatial_distance_cosine(item.embedding, query.embedding) } + ) + end, data) + + table.sort(data, function(a, b) return a.score > b.score end) - local result = {} - for i = 1, math.min(top_n, #scores) do - local srt = scores[i] - table.insert(result, vim.tbl_extend('force', data[srt.index], { score = srt.score })) - end - return result + + return vim.list_slice(data, 1, top_n) end --- Rank data by symbols @@ -150,6 +150,7 @@ local function data_ranked_by_symbols(query, data, top_n) table.sort(results, function(a, b) return a.score > b.score end) + return vim.list_slice(results, 1, top_n) end @@ -207,7 +208,6 @@ function M.outline(content, filename, ft) return output end - local lines = vim.split(content, '\n') local lang = vim.treesitter.language.get_lang(ft) local ok, parser = false, nil if lang then @@ -222,6 +222,7 @@ function M.outline(content, filename, ft) end local root = parser:parse()[1]:root() + local lines = vim.split(content, '\n') local outline_lines = {} local depth = 0 @@ -265,6 +266,7 @@ function M.outline(content, filename, ft) parse_node(root) if #outline_lines > 0 then + output.original = content output.content = table.concat(outline_lines, '\n') end @@ -408,51 +410,41 @@ function M.filter_embeddings(copilot, prompt, embeddings) return embeddings end - local original_map = utils.ordered_map() - local embedded_map = utils.ordered_map() - - -- Map embeddings by filename - for _, embed in ipairs(embeddings) do - original_map:set(embed.filename, embed) - embedded_map:set(embed.filename, M.outline(embed.content, embed.filename, embed.filetype)) - end + -- Map embeddings to outlines + embeddings = vim.tbl_map(function(embed) + return M.outline(embed.content, embed.filename, embed.filetype) + end, embeddings) -- Rank embeddings by symbols - local ranked_data = data_ranked_by_symbols(prompt, embedded_map:values(), TOP_SYMBOLS) - log.debug('Ranked data:', #ranked_data) - for i, item in ipairs(ranked_data) do + embeddings = data_ranked_by_symbols(prompt, embeddings, TOP_SYMBOLS) + log.debug('Ranked data:', #embeddings) + for i, item in ipairs(embeddings) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end -- Add prompt so it can be embedded - table.insert(ranked_data, { + table.insert(embeddings, { content = prompt, filename = 'prompt', filetype = 'raw', }) -- Get embeddings from all items - local embedded_data = copilot:embed(ranked_data) + embeddings = copilot:embed(embeddings) -- Rate embeddings by relatedness to the query - local embedded_query = table.remove(embedded_data, #embedded_data) + local embedded_query = table.remove(embeddings, #embeddings) log.debug('Embedded query:', embedded_query.content) - local ranked_embeddings = data_ranked_by_relatedness(embedded_query, embedded_data, TOP_RELATED) - log.debug('Ranked embeddings:', #ranked_embeddings) - for i, item in ipairs(ranked_embeddings) do + embeddings = data_ranked_by_relatedness(embedded_query, embeddings, TOP_RELATED) + log.debug('Ranked embeddings:', #embeddings) + for i, item in ipairs(embeddings) do log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) end - -- Return original content in ranked order - local result = {} - for _, ranked_item in ipairs(ranked_embeddings) do - local original = original_map:get(ranked_item.filename) - if original then - table.insert(result, original) - end - end - - return result + -- Return embeddings with original content + return vim.tbl_map(function(item) + return vim.tbl_extend('force', item, { content = item.original or item.content }) + end, embeddings) end return M From cf6f517331d505c18e774ca40000ece927c24c96 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Nov 2024 19:08:32 +0100 Subject: [PATCH 274/338] fix: improve sticky prompt handling and section detection This commit improves handling of sticky prompts by: 1. Using proper line splitting and startswith check instead of gmatch 2. Fixing the sticky prefix removal to only match start of line 3. Ensuring proper rendering before section/block detection The changes make sticky prompts more reliable and fix edge cases in the UI rendering. Closes #614 --- lua/CopilotChat/init.lua | 12 ++++++++---- lua/CopilotChat/ui/chat.lua | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7bd61285..cda465e0 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -287,11 +287,15 @@ local function finish(start_of_chat) state.chat:append(M.config.question_header .. M.config.separator .. '\n\n') + -- Reinsert sticky prompts from last prompt if state.last_prompt then local has_sticky = false - for sticky_line in state.last_prompt:gmatch('(>%s+[^\n]+)') do - state.chat:append(sticky_line .. '\n') - has_sticky = true + local lines = vim.split(state.last_prompt, '\n') + for _, line in ipairs(lines) do + if vim.startswith(line, '> ') then + state.chat:append(line .. '\n') + has_sticky = true + end end if has_sticky then state.chat:append('\n') @@ -666,7 +670,7 @@ function M.ask(prompt, config) -- Remove sticky prefix prompt = vim.trim(table.concat( vim.tbl_map(function(l) - return l:gsub('>%s+', '') + return l:gsub('^>%s+', '') end, vim.split(resolved_prompt, '\n')), '\n' )) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 6e10b014..7a9c14d6 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -242,6 +242,7 @@ function Chat:get_closest_section() return nil end + self:render() local cursor_pos = vim.api.nvim_win_get_cursor(self.winnr) local cursor_line = cursor_pos[1] local closest_section = nil @@ -279,6 +280,7 @@ function Chat:get_closest_block() return nil end + self:render() local cursor_pos = vim.api.nvim_win_get_cursor(self.winnr) local cursor_line = cursor_pos[1] local closest_block = nil @@ -318,7 +320,6 @@ function Chat:clear_prompt() end self:render() - local section = self.sections[#self.sections] if not section or section.answer then return From 793107ca796b0693c9378b349dea31c042aa3764 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Nov 2024 22:48:08 +0100 Subject: [PATCH 275/338] refactor: migrate to uv async for file operations Replace plenary async operations with vim.uv async APIs where possible to improve performance and reduce dependencies. This includes: - File operations now use vim.uv.fs_* functions - Simplified files context provider to remove pattern matching - Moved curl helpers to utils module - Cleaned up tiktoken loading process The file operations are still partly synchronous and marked with FIXME comments for future async migration. --- README.md | 2 +- lua/CopilotChat/config.lua | 12 ++---- lua/CopilotChat/context.lua | 25 ++++-------- lua/CopilotChat/copilot.lua | 42 ++++--------------- lua/CopilotChat/tiktoken.lua | 78 +++++++++++++++++------------------- lua/CopilotChat/utils.lua | 70 +++++++++++++++++++++++++++++++- 6 files changed, 124 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 500b6943..4a25b3f5 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ Default contexts are: - `buffer` - Includes specified buffer in chat context (default current). Supports input. - `buffers` - Includes all buffers in chat context (default listed). Supports input. - `file` - Includes content of provided file in chat context. Supports input. -- `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. +- `files` - Includes all non-hidden filenames in the current workspace in chat context. - `git` - Includes current git diff in chat context (default unstaged). Supports input. - `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 7f2cd97f..0fbe1ed5 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -228,15 +228,9 @@ return { end, }, files = { - description = 'Includes all non-hidden filenames in the current workspace in chat context. Supports input.', - input = function(callback) - vim.ui.input({ - prompt = 'Enter a file pattern> ', - default = '**/*', - }, callback) - end, - resolve = function(input, source) - return context.files(input, source.winnr) + description = 'Includes all non-hidden filenames in the current workspace in chat context.', + resolve = function(_, source) + return context.files(source.winnr) end, }, git = { diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index d3352746..ff21e50f 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -274,19 +274,14 @@ function M.outline(content, filename, ft) end --- Get list of all files in workspace ----@param pattern string? ---@param winnr number? ---@return table -function M.files(pattern, winnr) +function M.files(winnr) local cwd = utils.win_cwd(winnr) - local search = cwd .. '/' .. (pattern or '**/*') - local files = vim.tbl_filter(function(file) - return vim.fn.isdirectory(file) == 0 - end, vim.fn.glob(search, false, true)) - - if #files == 0 then - return {} - end + local files = utils.scan_dir(cwd, { + add_dirs = false, + respect_gitignore = true, + }) local out = {} @@ -315,17 +310,13 @@ end ---@param filename string ---@return CopilotChat.copilot.embed? function M.file(filename) - if vim.fn.filereadable(filename) ~= 1 then - return nil - end - - local content = vim.fn.readfile(filename) - if not content or #content == 0 then + local content = utils.read_file(filename) + if not content then return nil end return { - content = table.concat(content, '\n'), + content = content, filename = vim.fn.fnamemodify(filename, ':p:.'), filetype = vim.filetype.match({ filename = filename }), } diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index d880ae32..da89f569 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -18,9 +18,7 @@ ---@field model string? ---@field chunk_size number? -local async = require('plenary.async') local log = require('plenary.log') -local curl = require('plenary.curl') local prompts = require('CopilotChat.prompts') local tiktoken = require('CopilotChat.tiktoken') local utils = require('CopilotChat.utils') @@ -49,32 +47,6 @@ local VERSION_HEADERS = { -- ['x-github-api-version'] = '2023-07-07', } -local curl_get = async.wrap(function(url, opts, callback) - opts = vim.tbl_deep_extend('force', opts, { - callback = callback, - on_error = function(err) - err = err and err.stderr or vim.inspect(err) - callback(nil, err) - end, - }) - curl.get(url, opts) -end, 3) - -local curl_post = async.wrap(function(url, opts, callback) - opts = vim.tbl_deep_extend('force', opts, { - callback = callback, - on_error = function(err) - err = err and err.stderr or vim.inspect(err) - callback(nil, err) - end, - }) - curl.post(url, opts) -end, 3) - -local tiktoken_load = async.wrap(function(tokenizer, callback) - tiktoken.load(tokenizer, callback) -end, 2) - --- Get the github oauth cached token ---@return string|nil local function get_cached_token() @@ -384,7 +356,7 @@ function Copilot:authenticate() ['accept'] = 'application/json', }, VERSION_HEADERS) - local response, err = curl_get( + local response, err = utils.curl_get( 'https://api.github.com/copilot_internal/v2/token', vim.tbl_extend('force', self.request_args, { headers = headers, @@ -427,7 +399,7 @@ function Copilot:fetch_models() return self.models end - local response, err = curl_get( + local response, err = utils.curl_get( 'https://api.githubcopilot.com/models', vim.tbl_extend('force', self.request_args, { headers = self:authenticate(), @@ -468,7 +440,7 @@ function Copilot:fetch_agents() return self.agents end - local response, err = curl_get( + local response, err = utils.curl_get( 'https://api.githubcopilot.com/agents', vim.tbl_extend('force', self.request_args, { headers = self:authenticate(), @@ -504,7 +476,7 @@ function Copilot:enable_policy(model) return end - local response, err = curl_post( + local response, err = utils.curl_post( 'https://api.githubcopilot.com/models/' .. model .. '/policy', vim.tbl_extend('force', self.request_args, { headers = self:authenticate(), @@ -565,7 +537,7 @@ function Copilot:ask(prompt, opts) local tokenizer = capabilities.tokenizer log.debug('Max tokens: ' .. max_tokens) log.debug('Tokenizer: ' .. tokenizer) - tiktoken_load(tokenizer) + tiktoken.load(tokenizer) local generated_messages = {} local selection_messages = generate_selection_messages(selection) @@ -740,7 +712,7 @@ function Copilot:ask(prompt, opts) args.stream = stream_func end - local response, err = curl_post(url, args) + local response, err = utils.curl_post(url, args) if self.current_job ~= job_id then return nil, nil, nil @@ -902,7 +874,7 @@ function Copilot:embed(inputs, opts) for i = 1, #uncached_embeddings, chunk_size do local chunk = vim.list_slice(uncached_embeddings, i, i + chunk_size - 1) local body = vim.json.encode(generate_embedding_request(chunk, model)) - local response, err = curl_post( + local response, err = utils.curl_post( 'https://api.githubcopilot.com/embeddings', vim.tbl_extend('force', self.request_args, { headers = self:authenticate(), diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index e488455f..50f23df3 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,32 +1,23 @@ +local async = require('plenary.async') local curl = require('plenary.curl') local log = require('plenary.log') +local utils = require('CopilotChat.utils') local tiktoken_core = nil local current_tokenizer = nil - -local function get_cache_path(fname) - vim.fn.mkdir(tostring(vim.fn.stdpath('cache')), 'p') - return vim.fn.stdpath('cache') .. '/' .. fname -end - -local function file_exists(name) - local f = io.open(name, 'r') - if f ~= nil then - io.close(f) - return true - else - return false - end -end +local cache_dir = vim.fn.stdpath('cache') +vim.fn.mkdir(tostring(cache_dir), 'p') --- Load tiktoken data from cache or download it -local function load_tiktoken_data(done, tokenizer) +---@param tokenizer string The tokenizer to load +---@param on_done fun(path: string) The callback to call when the data is loaded +local function load_tiktoken_data(tokenizer, on_done) local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/' .. tokenizer .. '.tiktoken' - local cache_path = get_cache_path(tiktoken_url:match('.+/(.+)')) + local cache_path = cache_dir .. '/' .. tiktoken_url:match('.+/(.+)') - if file_exists(cache_path) then - done(cache_path) + if utils.file_exists(cache_path) then + on_done(cache_path) return end @@ -34,46 +25,46 @@ local function load_tiktoken_data(done, tokenizer) curl.get(tiktoken_url, { output = cache_path, callback = function() - done(cache_path) + on_done(cache_path) end, }) end local M = {} -function M.load(tokenizer, on_done) +--- Load the tiktoken module +---@param tokenizer string The tokenizer to load +M.load = async.wrap(function(tokenizer, callback) if tokenizer == current_tokenizer then - on_done() + callback() return end local ok, core = pcall(require, 'tiktoken_core') if not ok then - on_done() + callback() return end - vim.schedule(function() - load_tiktoken_data( - vim.schedule_wrap(function(path) - local special_tokens = {} - special_tokens['<|endoftext|>'] = 100257 - special_tokens['<|fim_prefix|>'] = 100258 - special_tokens['<|fim_middle|>'] = 100259 - special_tokens['<|fim_suffix|>'] = 100260 - special_tokens['<|endofprompt|>'] = 100276 - local pat_str = - "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+" - core.new(path, special_tokens, pat_str) - tiktoken_core = core - current_tokenizer = tokenizer - on_done() - end), - tokenizer - ) + load_tiktoken_data(tokenizer, function(path) + local special_tokens = {} + special_tokens['<|endoftext|>'] = 100257 + special_tokens['<|fim_prefix|>'] = 100258 + special_tokens['<|fim_middle|>'] = 100259 + special_tokens['<|fim_suffix|>'] = 100260 + special_tokens['<|endofprompt|>'] = 100276 + local pat_str = + "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+" + core.new(path, special_tokens, pat_str) + tiktoken_core = core + current_tokenizer = tokenizer + callback() end) -end +end, 2) +--- Encode a prompt +---@param prompt string The prompt to encode +---@return table? function M.encode(prompt) if not tiktoken_core then return nil @@ -88,6 +79,9 @@ function M.encode(prompt) return tiktoken_core.encode(prompt) end +--- Count the tokens in a prompt +---@param prompt string The prompt to count +---@return number function M.count(prompt) if not tiktoken_core then return math.ceil(#prompt * 0.5) -- Fallback to 1/2 character count diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 655b979a..305f210c 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,3 +1,7 @@ +local async = require('plenary.async') +local curl = require('plenary.curl') +local scandir = require('plenary.scandir') + local M = {} M.timers = {} @@ -212,7 +216,7 @@ end ---@param winnr number? The buffer number ---@return string function M.win_cwd(winnr) - if not winnr or not vim.api.nvim_win_is_valid(winnr) then + if not winnr then return '.' end @@ -224,4 +228,68 @@ function M.win_cwd(winnr) return dir end +--- Send curl get request +---@param url string The url +---@param opts table The options +M.curl_get = async.wrap(function(url, opts, callback) + curl.get( + url, + vim.tbl_deep_extend('force', opts, { + callback = callback, + on_error = function(err) + err = err and err.stderr or vim.inspect(err) + callback(nil, err) + end, + }) + ) +end, 3) + +--- Send curl post request +---@param url string The url +---@param opts table The options +M.curl_post = async.wrap(function(url, opts, callback) + curl.post( + url, + vim.tbl_deep_extend('force', opts, { + callback = callback, + on_error = function(err) + err = err and err.stderr or vim.inspect(err) + callback(nil, err) + end, + }) + ) +end, 3) + +--- Scan a directory +--- FIXME: Make async +M.scan_dir = scandir.scan_dir + +--- Check if a file exists +--- FIXME: Make async +---@param path string The file path +M.file_exists = function(path) + local stat = vim.uv.fs_stat(path) + return stat ~= nil +end + +--- Read a file +--- FIXME: Make async +---@param path string The file path +M.read_file = function(path) + local fd = vim.uv.fs_open(path, 'r', 438) + if not fd then + return nil + end + + local stat = vim.uv.fs_fstat(fd) + if not stat then + vim.uv.fs_close(fd) + return nil + end + + local data = vim.uv.fs_read(fd, stat.size, 0) + vim.uv.fs_close(fd) + return data +end + return M From a41083419f47ca809dd46d06c76dfa2da73f919d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 25 Nov 2024 21:49:37 +0000 Subject: [PATCH 276/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ef412d32..a17b4242 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -321,7 +321,7 @@ prompt by using `:` followed by the input (or pressing `complete` key after - `buffer` - Includes specified buffer in chat context (default current). Supports input. - `buffers` - Includes all buffers in chat context (default listed). Supports input. - `file` - Includes content of provided file in chat context. Supports input. -- `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input. +- `files` - Includes all non-hidden filenames in the current workspace in chat context. - `git` - Includes current git diff in chat context (default unstaged). Supports input. - `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. From f14aa9aeeb448f1b94a05e97ea3b53015893250b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 25 Nov 2024 22:59:08 +0100 Subject: [PATCH 277/338] refactor: optimize vim scheduler usage in chat module Remove redundant vim.schedule calls and replace them with more efficient async scheduler utilities. This includes: - Using async.util.scheduler() instead of vim.schedule wrapper functions - Consolidating scheduler calls to reduce overhead - Utilizing vim.schedule_wrap for progress callbacks The changes make the code more maintainable and potentially improve performance by reducing unnecessary scheduler invocations. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 88 ++++++++++++++++++--------------------- lua/CopilotChat/utils.lua | 10 +++++ test/plugin_spec.lua | 1 + 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index cda465e0..5faef520 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -521,9 +521,8 @@ function M.complete_items(callback) return a.kind < b.kind end) - vim.schedule(function() - callback(items) - end) + async.util.scheduler() + callback(items) end) end @@ -599,14 +598,13 @@ function M.select_model() return model end, models) - vim.schedule(function() - vim.ui.select(models, { - prompt = 'Select a model> ', - }, function(choice) - if choice then - M.config.model = choice:gsub(' %(selected%)', '') - end - end) + async.util.scheduler() + vim.ui.select(models, { + prompt = 'Select a model> ', + }, function(choice) + if choice then + M.config.model = choice:gsub(' %(selected%)', '') + end end) end) end @@ -623,14 +621,13 @@ function M.select_agent() return agent end, agents) - vim.schedule(function() - vim.ui.select(agents, { - prompt = 'Select an agent> ', - }, function(choice) - if choice then - M.config.agent = choice:gsub(' %(selected%)', '') - end - end) + async.util.scheduler() + vim.ui.select(agents, { + prompt = 'Select an agent> ', + }, function(choice) + if choice then + M.config.agent = choice:gsub(' %(selected%)', '') + end end) end) end @@ -675,7 +672,7 @@ function M.ask(prompt, config) '\n' )) - -- Resolve embeddings + -- Retrieve embeddings local embeddings, embedded_prompt = resolve_embeddings(prompt, config) prompt = embedded_prompt @@ -708,12 +705,11 @@ function M.ask(prompt, config) pcall(context.filter_embeddings, state.copilot, prompt, embeddings) if not query_ok then - vim.schedule(function() - log.error(vim.inspect(filtered_embeddings)) - if not config.headless then - show_error(filtered_embeddings, has_output) - end - end) + async.util.scheduler() + log.error(vim.inspect(filtered_embeddings)) + if not config.headless then + show_error(filtered_embeddings, has_output) + end return end @@ -726,23 +722,21 @@ function M.ask(prompt, config) agent = selected_agent, temperature = config.temperature, no_history = config.headless, - on_progress = function(token) - vim.schedule(function() - if not config.headless then - state.chat:append(token) - end - has_output = true - end) - end, + on_progress = vim.schedule_wrap(function(token) + if not config.headless then + state.chat:append(token) + end + has_output = true + end), }) + async.util.scheduler() + if not ask_ok then - vim.schedule(function() - log.error(vim.inspect(response)) - if not config.headless then - show_error(response, has_output) - end - end) + log.error(vim.inspect(response)) + if not config.headless then + show_error(response, has_output) + end return end @@ -756,14 +750,12 @@ function M.ask(prompt, config) state.chat.token_max_count = token_max_count end - vim.schedule(function() - if not config.headless then - finish() - end - if config.callback then - config.callback(response, state.source) - end - end) + if not config.headless then + finish() + end + if config.callback then + config.callback(response, state.source) + end end) end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 305f210c..e3143ea5 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -264,6 +264,16 @@ end, 3) --- FIXME: Make async M.scan_dir = scandir.scan_dir +-- M.scan_dir = async.wrap(function(path, opts, callback) +-- scandir.scan_dir_async(path, vim.tbl_deep_extend('force', opts, { +-- on_exit = callback, +-- on_error = function(err) +-- err = err and err.stderr or vim.inspect(err) +-- callback(nil, err) +-- end, +-- })) +-- end, 3) + --- Check if a file exists --- FIXME: Make async ---@param path string The file path diff --git a/test/plugin_spec.lua b/test/plugin_spec.lua index cb3e4b47..c09cdb88 100644 --- a/test/plugin_spec.lua +++ b/test/plugin_spec.lua @@ -8,6 +8,7 @@ package.loaded['plenary.async'] = { } package.loaded['plenary.curl'] = {} package.loaded['plenary.log'] = {} +package.loaded['plenary.scandir'] = {} describe('CopilotChat plugin', function() it('should be able to load', function() From 49edd2e21dfdc0f1bf4ed872099674cb8fe30280 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Nov 2024 00:01:18 +0100 Subject: [PATCH 278/338] refactor(mappings): improve key mapping API and descriptions Replace direct mapping key access with named mapping lookups to improve code organization. Add descriptive labels to keymaps using the plugin name and mapping name. This change makes the key mapping system more maintainable and provides better documentation in the key binding help. --- lua/CopilotChat/init.lua | 63 ++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 5faef520..febb2ee4 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -12,7 +12,7 @@ local Overlay = require('CopilotChat.ui.overlay') local Debug = require('CopilotChat.ui.debug') local M = {} -local PLUGIN_NAME = 'CopilotChat.nvim' +local PLUGIN_NAME = 'CopilotChat' local WORD = '([^%s]+)' --- @class CopilotChat.state @@ -327,15 +327,21 @@ local function show_error(err, append_newline) end --- Map a key to a function. ----@param key CopilotChat.config.mapping +---@param name string ---@param bufnr number ---@param fn function -local function map_key(key, bufnr, fn) +local function map_key(name, bufnr, fn) + local key = M.config.mappings[name] if not key then return end if key.normal and key.normal ~= '' then - vim.keymap.set('n', key.normal, fn, { buffer = bufnr, nowait = true }) + vim.keymap.set( + 'n', + key.normal, + fn, + { buffer = bufnr, nowait = true, desc = PLUGIN_NAME .. ' ' .. name:gsub('_', ' ') } + ) end if key.insert and key.insert ~= '' then vim.keymap.set('i', key.insert, function() @@ -352,16 +358,16 @@ local function map_key(key, bufnr, fn) else fn() end - end, { buffer = bufnr }) + end, { buffer = bufnr, desc = PLUGIN_NAME .. ' ' .. name:gsub('_', ' ') }) end end --- Get the info for a key. ---@param name string ----@param key CopilotChat.config.mapping? ---@param surround string|nil ---@return string -local function key_to_info(name, key, surround) +local function key_to_info(name, surround) + local key = M.config.mappings[name] if not key then return '' end @@ -907,8 +913,8 @@ function M.setup(config) { link = '@punctuation.special.markdown', default = true } ) - local overlay_help = key_to_info('close', M.config.mappings.close) - local diff_help = key_to_info('accept_diff', M.config.mappings.accept_diff) + local overlay_help = key_to_info('close') + local diff_help = key_to_info('accept_diff') if overlay_help ~= '' and diff_help ~= '' then diff_help = diff_help .. '\n' .. overlay_help end @@ -917,7 +923,7 @@ function M.setup(config) state.overlay:delete() end state.overlay = Overlay('copilot-overlay', overlay_help, function(bufnr) - map_key(M.config.mappings.close, bufnr, function() + map_key('close', bufnr, function() state.overlay:restore(state.chat.winnr, state.chat.bufnr) end) end) @@ -930,11 +936,11 @@ function M.setup(config) state.diff:delete() end state.diff = Diff(diff_help, function(bufnr) - map_key(M.config.mappings.close, bufnr, function() + map_key('close', bufnr, function() state.diff:restore(state.chat.winnr, state.chat.bufnr) end) - map_key(M.config.mappings.accept_diff, bufnr, function() + map_key('accept_diff', bufnr, function() apply_diff(state.diff:get_diff(), state.chat.config) end) end) @@ -947,9 +953,9 @@ function M.setup(config) M.config.question_header, M.config.answer_header, M.config.separator, - key_to_info('show_help', M.config.mappings.show_help), + key_to_info('show_help'), function(bufnr) - map_key(M.config.mappings.show_help, bufnr, function() + map_key('show_help', bufnr, function() local chat_help = '**`Special tokens`**\n' chat_help = chat_help .. '`@` to select an agent\n' chat_help = chat_help .. '`#` to select a context\n' @@ -969,8 +975,7 @@ function M.setup(config) end) for _, name in ipairs(chat_keys) do if name ~= 'close' then - local key = M.config.mappings[name] - local info = key_to_info(name, key, '`') + local info = key_to_info(name, '`') if info ~= '' then chat_help = chat_help .. info .. '\n' end @@ -979,11 +984,11 @@ function M.setup(config) state.overlay:show(chat_help, state.chat.winnr, 'markdown') end) - map_key(M.config.mappings.reset, bufnr, M.reset) - map_key(M.config.mappings.close, bufnr, M.close) - map_key(M.config.mappings.complete, bufnr, trigger_complete) + map_key('reset', bufnr, M.reset) + map_key('close', bufnr, M.close) + map_key('complete', bufnr, trigger_complete) - map_key(M.config.mappings.submit_prompt, bufnr, function() + map_key('submit_prompt', bufnr, function() local section = state.chat:get_closest_section() if not section or section.answer then return @@ -992,7 +997,7 @@ function M.setup(config) M.ask(section.content) end) - map_key(M.config.mappings.toggle_sticky, bufnr, function() + map_key('toggle_sticky', bufnr, function() local section = state.chat:get_closest_section() if not section or section.answer then return @@ -1036,11 +1041,11 @@ function M.setup(config) vim.api.nvim_win_set_cursor(0, cursor) end) - map_key(M.config.mappings.accept_diff, bufnr, function() + map_key('accept_diff', bufnr, function() apply_diff(get_diff(state.chat.config), state.chat.config) end) - map_key(M.config.mappings.jump_to_diff, bufnr, function() + map_key('jump_to_diff', bufnr, function() if not state.source or not state.source.winnr @@ -1083,7 +1088,7 @@ function M.setup(config) ) end) - map_key(M.config.mappings.quickfix_diffs, bufnr, function() + map_key('quickfix_diffs', bufnr, function() local selection = get_selection(state.chat.config) local items = {} @@ -1115,7 +1120,7 @@ function M.setup(config) vim.cmd('copen') end) - map_key(M.config.mappings.yank_diff, bufnr, function() + map_key('yank_diff', bufnr, function() local diff = get_diff(state.chat.config) if not diff then return @@ -1124,7 +1129,7 @@ function M.setup(config) vim.fn.setreg(M.config.mappings.yank_diff.register, diff.change) end) - map_key(M.config.mappings.show_diff, bufnr, function() + map_key('show_diff', bufnr, function() local diff = get_diff(state.chat.config) if not diff then return @@ -1133,7 +1138,7 @@ function M.setup(config) state.diff:show(diff, state.chat.winnr) end) - map_key(M.config.mappings.show_system_prompt, bufnr, function() + map_key('show_system_prompt', bufnr, function() local section = state.chat:get_closest_section() local system_prompt = state.chat.config.system_prompt if section and not section.answer then @@ -1146,7 +1151,7 @@ function M.setup(config) state.overlay:show(vim.trim(system_prompt) .. '\n', state.chat.winnr, 'markdown') end) - map_key(M.config.mappings.show_user_selection, bufnr, function() + map_key('show_user_selection', bufnr, function() local selection = get_selection(state.chat.config) if not selection then return @@ -1155,7 +1160,7 @@ function M.setup(config) state.overlay:show(selection.content, state.chat.winnr, selection.filetype) end) - map_key(M.config.mappings.show_user_context, bufnr, function() + map_key('show_user_context', bufnr, function() local section = state.chat:get_closest_section() local embeddings = {} if section and not section.answer then From d2a06235b6a7ba4192186d6ddcb4938b7ec1d42a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Nov 2024 22:07:20 +0100 Subject: [PATCH 279/338] perf: optimize curl connection settings Improve performance of HTTP requests by: - Enabling keepalive connections with 60s timeout - Disabling compression since responses are already streamed - Maintaining TCP nodelay and no-buffer settings for efficient streaming - Adding explicit timeouts for better connection handling The changes aim to reduce latency and improve overall request efficiency while maintaining reliable streaming behavior. --- lua/CopilotChat/copilot.lua | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index da89f569..210db30c 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -322,15 +322,19 @@ local Copilot = class(function(self, proxy, allow_insecure) -- Wait 1 second between retries '--retry-delay', '1', - -- Maximum time for the request + -- Keep connections alive for better performance + '--keepalive-time', + '60', + -- Disable compression (since responses are already streamed efficiently) + '--no-compressed', + -- Important timeouts '--max-time', math.floor(TIMEOUT * 2 / 1000), - -- Timeout for initial connection '--connect-timeout', '10', - '--no-keepalive', -- Don't reuse connections - '--tcp-nodelay', -- Disable Nagle's algorithm for faster streaming - '--no-buffer', -- Disable output buffering for streaming + -- Streaming optimizations + '--tcp-nodelay', + '--no-buffer', }, } end) From 6b372d4cbd77b932344a2b2ea1b904e9469cf7f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 26 Nov 2024 21:10:10 +0000 Subject: [PATCH 280/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index a17b4242..97750af7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 25 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 26 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 9087fb8495ec425e681944f186c837c4e09361bf Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Nov 2024 22:15:10 +0100 Subject: [PATCH 281/338] fix: add debug logging for Copilot HTTP responses Add detailed debug logging for Copilot HTTP response status, body and headers to help with troubleshooting API communication issues. --- lua/CopilotChat/copilot.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 210db30c..838035be 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -734,6 +734,10 @@ function Copilot:ask(prompt, opts) return end + log.debug('Response status: ' .. response.status) + log.debug('Response body: ' .. response.body) + log.debug('Response headers: ' .. vim.inspect(response.headers)) + if response.status ~= 200 then if response.status == 401 then local ok, content = pcall(vim.json.decode, response.body, { From 3968c25e9418a544d5faa3c242d577526b0710e7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 26 Nov 2024 23:45:55 +0100 Subject: [PATCH 282/338] fix: parse stream response body again if stream returned no response Sometimes for some reason the stream_func isnt called properly, in that case the stream content might be in body so try to parse final response body again. See #619 Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 47 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 838035be..0060ae41 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -211,8 +211,9 @@ local function generate_ask_request( max_output_tokens, stream ) + local is_o1 = vim.startswith(model, 'o1') local messages = {} - local system_role = stream and 'system' or 'user' + local system_role = is_o1 and 'user' or 'system' local contexts = {} if system_prompt ~= '' then @@ -250,14 +251,14 @@ local function generate_ask_request( messages = messages, model = model, stream = stream, + n = 1, } if max_output_tokens then out.max_tokens = max_output_tokens end - if stream then - out.n = 1 + if not is_o1 then out.temperature = temperature out.top_p = 1 end @@ -655,36 +656,42 @@ function Copilot:ask(prompt, opts) full_response = full_response .. content end - local function stream_func(err, line, job) - if not line or errored or finished then + local function parse_stream_line(line, job) + line = vim.trim(line) + if not vim.startswith(line, 'data: ') then return end + line = line:gsub('^data:%s*', '') - if self.current_job ~= job_id then - finish_stream(nil, job) + if line == '[DONE]' then + if job then + finish_stream(nil, job) + end return end - if err or vim.startswith(line, '{"error"') then - finish_stream('Failed to get response: ' .. (err and vim.inspect(err) or line), job) - return + local err = parse_line(line) + if err and job then + finish_stream('Failed to parse response: ' .. vim.inspect(err) .. '\n' .. line, job) end + end - if not vim.startswith(line, 'data: ') then + local function stream_func(err, line, job) + if not line or errored or finished then return end - line = line:gsub('^%s*data:%s*', ''):gsub('%s*$', '') - - if line == '[DONE]' then + if self.current_job ~= job_id then finish_stream(nil, job) return end - err = parse_line(line) if err then - finish_stream('Failed to parse response: ' .. vim.inspect(err) .. '\n' .. line, job) + finish_stream('Failed to get response: ' .. (err and vim.inspect(err) or line), job) + return end + + parse_stream_line(line, job) end local is_stream = not vim.startswith(model, 'o1') @@ -767,7 +774,13 @@ function Copilot:ask(prompt, opts) return end - if not is_stream then + if is_stream then + if full_response == '' then + for _, line in ipairs(vim.split(response.body, '\n')) do + parse_stream_line(line) + end + end + else parse_line(response.body) end From 3c6b463da63ac0a098b9b671faf958f6413aa784 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 27 Nov 2024 22:08:20 +0100 Subject: [PATCH 283/338] fix: make file operations and contexts async The changes make various file operations and context resolution properly async by: - Converting synchronous UV calls to async versions - Moving context resolution inside async block - Making tiktoken data loading fully async - Converting directory scanning to async version - Adding proper error handling for async UV operations This improves overall responsiveness of the plugin by preventing blocking operations from freezing the editor. Signed-off-by: Tomas Slusny --- .luarc.json | 3 +- lua/CopilotChat/context.lua | 4 ++ lua/CopilotChat/init.lua | 71 +++++++++++++++++++----------------- lua/CopilotChat/tiktoken.lua | 46 ++++++++++------------- lua/CopilotChat/utils.lua | 49 +++++++++++++------------ 5 files changed, 89 insertions(+), 84 deletions(-) diff --git a/.luarc.json b/.luarc.json index 04ea467f..b97a9f11 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,3 +1,4 @@ { - "diagnostics.globals": ["describe", "it"] + "diagnostics.globals": ["describe", "it"], + "diagnostics.disable": ["redefined-local"] } diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index ff21e50f..b7916709 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -10,6 +10,7 @@ ---@class CopilotChat.context.outline : CopilotChat.copilot.embed ---@field symbols table +local async = require('plenary.async') local log = require('plenary.log') local utils = require('CopilotChat.utils') @@ -315,6 +316,7 @@ function M.file(filename) return nil end + async.util.scheduler() return { content = content, filename = vim.fn.fnamemodify(filename, ':p:.'), @@ -326,6 +328,8 @@ end ---@param bufnr number ---@return CopilotChat.copilot.embed? function M.buffer(bufnr) + async.util.scheduler() + if not utils.buf_valid(bufnr) then return nil end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index febb2ee4..83cd38e9 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -233,24 +233,16 @@ end ---@param config CopilotChat.config.shared ---@return table, string local function resolve_embeddings(prompt, config) - local embeddings = utils.ordered_map() - + local contexts = {} local function parse_context(prompt_context) local split = vim.split(prompt_context, ':') local context_name = table.remove(split, 1) local context_input = vim.trim(table.concat(split, ':')) - local context_value = M.config.contexts[context_name] - if context_input == '' then - ---@diagnostic disable-next-line: cast-local-type - context_input = nil - end - - if context_value then - for _, embedding in ipairs(context_value.resolve(context_input, state.source)) do - if embedding then - embeddings:set(embedding.filename, embedding) - end - end + if M.config.contexts[context_name] then + table.insert(contexts, { + name = context_name, + input = (context_input ~= '' and context_input or nil), + }) return true end @@ -276,6 +268,16 @@ local function resolve_embeddings(prompt, config) end end + local embeddings = utils.ordered_map() + for _, context_data in ipairs(contexts) do + local context_value = M.config.contexts[context_data.name] + for _, embedding in ipairs(context_value.resolve(context_data.input, state.source)) do + if embedding then + embeddings:set(embedding.filename, embedding) + end + end + end + return embeddings:values(), prompt end @@ -678,14 +680,13 @@ function M.ask(prompt, config) '\n' )) - -- Retrieve embeddings - local embeddings, embedded_prompt = resolve_embeddings(prompt, config) - prompt = embedded_prompt - -- Retrieve the selection local selection = get_selection(config) async.run(function() + local embeddings, embedded_prompt = resolve_embeddings(prompt, config) + prompt = embedded_prompt + local agents = vim.tbl_keys(state.copilot:list_agents()) local selected_agent = config.agent prompt = prompt:gsub('@' .. WORD, function(match) @@ -1162,25 +1163,29 @@ function M.setup(config) map_key('show_user_context', bufnr, function() local section = state.chat:get_closest_section() - local embeddings = {} - if section and not section.answer then - embeddings = resolve_embeddings(section.content, state.chat.config) - end - local text = '' - for _, embedding in ipairs(embeddings) do - local lines = vim.split(embedding.content, '\n') - local preview = table.concat(vim.list_slice(lines, 1, math.min(10, #lines)), '\n') - local header = string.format('**`%s`** (%s lines)', embedding.filename, #lines) - if #lines > 10 then - header = header .. ' (truncated)' + async.run(function() + local embeddings = {} + if section and not section.answer then + embeddings = resolve_embeddings(section.content, state.chat.config) end - text = text - .. string.format('%s\n```%s\n%s\n```\n\n', header, embedding.filetype, preview) - end + local text = '' + for _, embedding in ipairs(embeddings) do + local lines = vim.split(embedding.content, '\n') + local preview = table.concat(vim.list_slice(lines, 1, math.min(10, #lines)), '\n') + local header = string.format('**`%s`** (%s lines)', embedding.filename, #lines) + if #lines > 10 then + header = header .. ' (truncated)' + end - state.overlay:show(vim.trim(text) .. '\n', state.chat.winnr, 'markdown') + text = text + .. string.format('%s\n```%s\n%s\n```\n\n', header, embedding.filetype, preview) + end + + async.util.scheduler() + state.overlay:show(vim.trim(text) .. '\n', state.chat.winnr, 'markdown') + end) end) vim.api.nvim_create_autocmd({ 'BufEnter', 'BufLeave' }, { diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 50f23df3..ac6202af 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,5 +1,4 @@ local async = require('plenary.async') -local curl = require('plenary.curl') local log = require('plenary.log') local utils = require('CopilotChat.utils') local tiktoken_core = nil @@ -9,58 +8,51 @@ vim.fn.mkdir(tostring(cache_dir), 'p') --- Load tiktoken data from cache or download it ---@param tokenizer string The tokenizer to load ----@param on_done fun(path: string) The callback to call when the data is loaded -local function load_tiktoken_data(tokenizer, on_done) +local function load_tiktoken_data(tokenizer) local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/' .. tokenizer .. '.tiktoken' local cache_path = cache_dir .. '/' .. tiktoken_url:match('.+/(.+)') if utils.file_exists(cache_path) then - on_done(cache_path) - return + return cache_path end log.info('Downloading tiktoken data from ' .. tiktoken_url) - curl.get(tiktoken_url, { + utils.curl_get(tiktoken_url, { output = cache_path, - callback = function() - on_done(cache_path) - end, }) + + return cache_path end local M = {} --- Load the tiktoken module ---@param tokenizer string The tokenizer to load -M.load = async.wrap(function(tokenizer, callback) +M.load = function(tokenizer) if tokenizer == current_tokenizer then - callback() return end local ok, core = pcall(require, 'tiktoken_core') if not ok then - callback() return end - load_tiktoken_data(tokenizer, function(path) - local special_tokens = {} - special_tokens['<|endoftext|>'] = 100257 - special_tokens['<|fim_prefix|>'] = 100258 - special_tokens['<|fim_middle|>'] = 100259 - special_tokens['<|fim_suffix|>'] = 100260 - special_tokens['<|endofprompt|>'] = 100276 - local pat_str = - "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+" - core.new(path, special_tokens, pat_str) - tiktoken_core = core - current_tokenizer = tokenizer - callback() - end) -end, 2) + local path = load_tiktoken_data(tokenizer) + local special_tokens = {} + special_tokens['<|endoftext|>'] = 100257 + special_tokens['<|fim_prefix|>'] = 100258 + special_tokens['<|fim_middle|>'] = 100259 + special_tokens['<|fim_suffix|>'] = 100260 + special_tokens['<|endofprompt|>'] = 100276 + local pat_str = + "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+" + core.new(path, special_tokens, pat_str) + tiktoken_core = core + current_tokenizer = tokenizer +end --- Encode a prompt ---@param prompt string The prompt to encode diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index e3143ea5..95a5418a 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -261,44 +261,47 @@ M.curl_post = async.wrap(function(url, opts, callback) end, 3) --- Scan a directory ---- FIXME: Make async -M.scan_dir = scandir.scan_dir - --- M.scan_dir = async.wrap(function(path, opts, callback) --- scandir.scan_dir_async(path, vim.tbl_deep_extend('force', opts, { --- on_exit = callback, --- on_error = function(err) --- err = err and err.stderr or vim.inspect(err) --- callback(nil, err) --- end, --- })) --- end, 3) +---@param path string The directory path +---@param opts table The options +M.scan_dir = async.wrap(function(path, opts, callback) + scandir.scan_dir_async( + path, + vim.tbl_deep_extend('force', opts, { + on_exit = callback, + on_error = function(err) + err = err and err.stderr or vim.inspect(err) + callback(nil, err) + end, + }) + ) +end, 3) --- Check if a file exists ---- FIXME: Make async ---@param path string The file path M.file_exists = function(path) - local stat = vim.uv.fs_stat(path) - return stat ~= nil + local err, stat = async.uv.fs_stat(path) + return err == nil and stat ~= nil end --- Read a file ---- FIXME: Make async ---@param path string The file path M.read_file = function(path) - local fd = vim.uv.fs_open(path, 'r', 438) - if not fd then + local err, fd = async.uv.fs_open(path, 'r', 438) + if err or not fd then return nil end - local stat = vim.uv.fs_fstat(fd) - if not stat then - vim.uv.fs_close(fd) + local err, stat = async.uv.fs_fstat(fd) + if err or not stat then + async.uv.fs_close(fd) return nil end - local data = vim.uv.fs_read(fd, stat.size, 0) - vim.uv.fs_close(fd) + local err, data = async.uv.fs_read(fd, stat.size, 0) + async.uv.fs_close(fd) + if err or not data then + return nil + end return data end From ebd40d106da3433f9a50f14bb887a9d029898929 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 Nov 2024 21:11:41 +0000 Subject: [PATCH 284/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 97750af7..6ec18b48 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 26 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 27 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 40eb8011373c78465c12b52d74b7dda24f1c8109 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 27 Nov 2024 22:29:57 +0100 Subject: [PATCH 285/338] refactor(context): replace io.popen with vim.system Replace synchronous io.popen git diff call with asynchronous vim.system in context.lua. Add async scheduler to token fetching and introduce a new utils helper for system calls. This change improves overall plugin performance by making git operations asynchronous and following Neovim's best practices for system calls. --- lua/CopilotChat/context.lua | 24 +++++++++++------------- lua/CopilotChat/copilot.lua | 3 +++ lua/CopilotChat/utils.lua | 6 ++++++ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index b7916709..b6e0b755 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -353,25 +353,23 @@ end function M.gitdiff(type, winnr) type = type or 'unstaged' local cwd = utils.win_cwd(winnr) - local cmd = 'git -C ' .. cwd .. ' diff --no-color --no-ext-diff' + local cmd = { + 'git', + '-C', + cwd, + 'diff', + '--no-color', + '--no-ext-diff', + } if type == 'staged' then - cmd = cmd .. ' --staged' - end - - local handle = io.popen(cmd) - if not handle then - return nil + table.insert(cmd, '--staged') end - local result = handle:read('*a') - handle:close() - if not result or result == '' then - return nil - end + local out = utils.system(cmd) return { - content = result, + content = out.stdout, filename = 'git_diff_' .. type, filetype = 'diff', } diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 0060ae41..aab96c70 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -18,6 +18,7 @@ ---@field model string? ---@field chunk_size number? +local async = require('plenary.async') local log = require('plenary.log') local prompts = require('CopilotChat.prompts') local tiktoken = require('CopilotChat.tiktoken') @@ -50,6 +51,8 @@ local VERSION_HEADERS = { --- Get the github oauth cached token ---@return string|nil local function get_cached_token() + async.util.scheduler() + -- loading token from the environment only in GitHub Codespaces local token = os.getenv('GITHUB_TOKEN') local codespaces = os.getenv('CODESPACES') diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 95a5418a..a06a7829 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -305,4 +305,10 @@ M.read_file = function(path) return data end +--- Call a system command +---@param cmd table The command +M.system = async.wrap(function(cmd, callback) + vim.system(cmd, { text = true }, callback) +end, 2) + return M From 5c82561f46e0bc560fd4743974297ec021c75e61 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 27 Nov 2024 22:33:12 +0100 Subject: [PATCH 286/338] perf: optimize GitHub token handling and remove scheduler Remove unnecessary async scheduler call from get_cached_token function and move github_token initialization to constructor to avoid repeated calls. This change improves the performance by reducing overhead of token retrieval. --- lua/CopilotChat/copilot.lua | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index aab96c70..e7380df7 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -18,7 +18,6 @@ ---@field model string? ---@field chunk_size number? -local async = require('plenary.async') local log = require('plenary.log') local prompts = require('CopilotChat.prompts') local tiktoken = require('CopilotChat.tiktoken') @@ -51,8 +50,6 @@ local VERSION_HEADERS = { --- Get the github oauth cached token ---@return string|nil local function get_cached_token() - async.util.scheduler() - -- loading token from the environment only in GitHub Codespaces local token = os.getenv('GITHUB_TOKEN') local codespaces = os.getenv('CODESPACES') @@ -314,6 +311,7 @@ local Copilot = class(function(self, proxy, allow_insecure) self.token = nil self.sessionid = nil self.machineid = utils.machine_id() + self.github_token = get_cached_token() self.request_args = { timeout = TIMEOUT, @@ -347,12 +345,9 @@ end) ---@return table function Copilot:authenticate() if not self.github_token then - self.github_token = get_cached_token() - if not self.github_token then - error( - 'No GitHub token found, please use `:Copilot auth` to set it up from copilot.lua or `:Copilot setup` for copilot.vim' - ) - end + error( + 'No GitHub token found, please use `:Copilot auth` to set it up from copilot.lua or `:Copilot setup` for copilot.vim' + ) end if From ed7234cdab44c4b64c4ad4e6baa3b1ba987bc0ef Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Nov 2024 01:23:05 +0100 Subject: [PATCH 287/338] feat(context): add full file content support for files The files context provider now supports two modes: - 'list' (default) - includes only filenames in the workspace - 'full' - includes both filenames and their content This change provides more flexibility when needing full context from workspace files. The implementation also adds proper filetype detection and filtering for files to ensure only text files are included. Closes #360 Signed-off-by: Tomas Slusny --- README.md | 4 ++-- lua/CopilotChat/config.lua | 11 +++++++--- lua/CopilotChat/context.lua | 43 ++++++++++++++++++++++++++++++++----- lua/CopilotChat/copilot.lua | 4 +--- lua/CopilotChat/utils.lua | 18 ++++++++++++++++ 5 files changed, 67 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4a25b3f5..bdc5a622 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ Default contexts are: - `buffer` - Includes specified buffer in chat context (default current). Supports input. - `buffers` - Includes all buffers in chat context (default listed). Supports input. - `file` - Includes content of provided file in chat context. Supports input. -- `files` - Includes all non-hidden filenames in the current workspace in chat context. +- `files` - Includes all non-hidden files in the current workspace in chat context. By default includes only filenames, includes also content with `full` input. Including all content can be slow on big workspaces so use with care. Supports input. - `git` - Includes current git diff in chat context (default unstaged). Supports input. - `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. @@ -719,7 +719,7 @@ require('CopilotChat').setup({ ## Roadmap (Wishlist) -- Use indexed vector database with current workspace for better context selection +- Caching for contexts - General QOL improvements ## Development diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 0fbe1ed5..f92edbd3 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -228,9 +228,14 @@ return { end, }, files = { - description = 'Includes all non-hidden filenames in the current workspace in chat context.', - resolve = function(_, source) - return context.files(source.winnr) + description = 'Includes all non-hidden files in the current workspace in chat context. By default includes only filenames, includes also content with `full` input. Including all content can be slow on big workspaces so use with care. Supports input.', + input = function(callback) + vim.ui.select({ 'list', 'full' }, { + prompt = 'Select files content> ', + }, callback) + end, + resolve = function(input, source) + return context.files(source.winnr, input == 'full') end, }, git = { diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index b6e0b755..8d0fd9d4 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -276,8 +276,9 @@ end --- Get list of all files in workspace ---@param winnr number? +---@param with_content boolean? ---@return table -function M.files(winnr) +function M.files(winnr, with_content) local cwd = utils.win_cwd(winnr) local files = utils.scan_dir(cwd, { add_dirs = false, @@ -286,7 +287,35 @@ function M.files(winnr) local out = {} - -- Create embeddings in chunks + -- Read all files if we want content as well + if with_content then + async.util.scheduler() + + files = vim.tbl_map(function(file) + return { + name = utils.filepath(file), + ft = utils.filetype(file), + } + end, files) + files = vim.tbl_filter(function(file) + return file.ft ~= nil + end, files) + + for _, file in ipairs(files) do + local content = utils.read_file(file.name) + if content then + table.insert(out, { + content = content, + filename = file.name, + filetype = file.ft, + }) + end + end + + return out + end + + -- Create file list in chunks local chunk_size = 100 for i = 1, #files, chunk_size do local chunk = {} @@ -317,10 +346,14 @@ function M.file(filename) end async.util.scheduler() + if not utils.filetype(filename) then + return nil + end + return { content = content, - filename = vim.fn.fnamemodify(filename, ':p:.'), - filetype = vim.filetype.match({ filename = filename }), + filename = utils.filepath(filename), + filetype = utils.filetype(filename), } end @@ -341,7 +374,7 @@ function M.buffer(bufnr) return { content = table.concat(content, '\n'), - filename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ':p:.'), + filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), filetype = vim.bo[bufnr].filetype, } end diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index e7380df7..916b73c8 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -329,9 +329,7 @@ local Copilot = class(function(self, proxy, allow_insecure) '60', -- Disable compression (since responses are already streamed efficiently) '--no-compressed', - -- Important timeouts - '--max-time', - math.floor(TIMEOUT * 2 / 1000), + -- Connect timeout of 10 seconds '--connect-timeout', '10', -- Streaming optimizations diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index a06a7829..64349bf9 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -180,6 +180,24 @@ function M.filename_same(file1, file2) return vim.fn.fnamemodify(file1, ':p') == vim.fn.fnamemodify(file2, ':p') end +--- Get the filetype of a file +---@param filename string The file name +---@return string|nil +function M.filetype(filename) + local ft = vim.filetype.match({ filename = filename }) + if ft == '' then + return nil + end + return ft +end + +--- Get the file path +---@param filename string The file name +---@return string +function M.filepath(filename) + return vim.fn.fnamemodify(filename, ':p:.') +end + --- Generate a UUID ---@return string function M.uuid() From dee009062a5941d77285910570ac83f92209952b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Nov 2024 00:36:35 +0000 Subject: [PATCH 288/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 6ec18b48..ff83a160 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 27 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 28 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -321,7 +321,7 @@ prompt by using `:` followed by the input (or pressing `complete` key after - `buffer` - Includes specified buffer in chat context (default current). Supports input. - `buffers` - Includes all buffers in chat context (default listed). Supports input. - `file` - Includes content of provided file in chat context. Supports input. -- `files` - Includes all non-hidden filenames in the current workspace in chat context. +- `files` - Includes all non-hidden files in the current workspace in chat context. By default includes only filenames, includes also content with `full` input. Including all content can be slow on big workspaces so use with care. Supports input. - `git` - Includes current git diff in chat context (default unstaged). Supports input. - `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. @@ -757,7 +757,7 @@ installed. ROADMAP (WISHLIST) *CopilotChat-roadmap-(wishlist)* -- Use indexed vector database with current workspace for better context selection +- Caching for contexts - General QOL improvements From 8584c535f4598314bb812f3a46262a096c160b87 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Nov 2024 09:26:49 +0100 Subject: [PATCH 289/338] refactor: improve embedding stability and token handling This commit improves the stability and efficiency of the embedding system: - Add token count validation before processing embeddings - Implement smarter batching based on token limits (8191 max) - Reduce line threshold for big embeds from 600 to 500 - Improve error handling and logging for embedding failures - Optimize tiktoken loading and caching --- lua/CopilotChat/copilot.lua | 150 +++++++++++++++++++---------------- lua/CopilotChat/tiktoken.lua | 11 ++- 2 files changed, 86 insertions(+), 75 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 916b73c8..062500e8 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -28,7 +28,10 @@ local temp_file = utils.temp_file --- Constants local CONTEXT_FORMAT = '[#file:%s](#file:%s-context)' local BIG_FILE_THRESHOLD = 2000 -local BIG_EMBED_THRESHOLD = 600 +local BIG_EMBED_THRESHOLD = 500 +local EMBED_MODEL = 'text-embedding-3-small' +local EMBED_MAX_TOKENS = 8191 +local EMBED_TOKENIZER = 'cl100k_base' local TRUNCATED = '... (truncated)' local TIMEOUT = 30000 local VERSION_HEADERS = { @@ -266,23 +269,30 @@ local function generate_ask_request( return out end +local function generate_embeddings_request_message(embedding) + local lines = vim.split(embedding.content, '\n') + if #lines > BIG_EMBED_THRESHOLD then + lines = vim.list_slice(lines, 1, BIG_EMBED_THRESHOLD) + table.insert(lines, TRUNCATED) + end + local content = table.concat(lines, '\n') + + if embedding.filetype == 'raw' then + return content + else + return string.format( + 'File: `%s`\n```%s\n%s\n```', + embedding.filename, + embedding.filetype, + content + ) + end +end + local function generate_embedding_request(inputs, model) return { dimensions = 512, - input = vim.tbl_map(function(input) - local lines = vim.split(input.content, '\n') - if #lines > BIG_EMBED_THRESHOLD then - lines = vim.list_slice(lines, 1, BIG_EMBED_THRESHOLD) - table.insert(lines, TRUNCATED) - end - local content = table.concat(lines, '\n') - - if input.filetype == 'raw' then - return content - else - return string.format('File: `%s`\n```%s\n%s\n```', input.filename, input.filetype, content) - end - end, inputs), + input = vim.tbl_map(generate_embeddings_request_message, inputs), model = model, } end @@ -856,41 +866,65 @@ end --- Generate embeddings for the given inputs ---@param inputs table: The inputs to embed ----@param opts CopilotChat.copilot.embed.opts?: Options for the request ---@return table function Copilot:embed(inputs, opts) if not inputs or #inputs == 0 then return {} end - -- Check which embeddings need to be fetched - local cached_embeddings = {} - local uncached_embeddings = {} - for _, embed in ipairs(inputs) do - embed.filename = embed.filename or 'unknown' - embed.filetype = embed.filetype or 'text' - - if embed.content then - local key = embed.filename .. utils.quick_hash(embed.content) - if self.embedding_cache[key] then - table.insert(cached_embeddings, self.embedding_cache[key]) + -- Initialize essentials + local model = EMBED_MODEL + tiktoken.load(EMBED_TOKENIZER) + local to_process = {} + local results = {} + + -- Process each input, using cache when possible + for _, input in ipairs(inputs) do + input.filename = input.filename or 'unknown' + input.filetype = input.filetype or 'text' + + if input.content then + local cache_key = input.filename .. utils.quick_hash(input.content) + if self.embedding_cache[cache_key] then + table.insert(results, self.embedding_cache[cache_key]) else - table.insert(uncached_embeddings, embed) + local message = generate_embeddings_request_message(input) + local tokens = tiktoken.count(message) + + if tokens <= EMBED_MAX_TOKENS then + input.tokens = tokens + table.insert(to_process, input) + else + log.warn( + string.format( + 'Embedding for %s exceeds token limit (%d > %d), skipping', + input.filename, + tokens, + EMBED_MAX_TOKENS + ) + ) + end end - else - table.insert(uncached_embeddings, embed) end end - opts = opts or {} - local model = opts.model or 'text-embedding-3-small' - local chunk_size = opts.chunk_size or 15 + -- Process inputs in batches + while #to_process > 0 do + local batch = {} + local batch_tokens = 0 - local out = {} + -- Build batch within token limit + while #to_process > 0 do + local next_input = to_process[1] + if batch_tokens + next_input.tokens > EMBED_MAX_TOKENS then + break + end + table.insert(batch, table.remove(to_process, 1)) + batch_tokens = batch_tokens + next_input.tokens + end - for i = 1, #uncached_embeddings, chunk_size do - local chunk = vim.list_slice(uncached_embeddings, i, i + chunk_size - 1) - local body = vim.json.encode(generate_embedding_request(chunk, model)) + -- Get embeddings for batch + local body = vim.json.encode(generate_embedding_request(batch, model)) local response, err = utils.curl_post( 'https://api.githubcopilot.com/embeddings', vim.tbl_extend('force', self.request_args, { @@ -899,48 +933,26 @@ function Copilot:embed(inputs, opts) }) ) - if err then - error(err) - return {} - end - - if not response then - error('Failed to get response') - return {} - end - - if response.status ~= 200 then - error('Failed to get response: ' .. tostring(response.status) .. '\n' .. response.body) - return {} + if err or not response or response.status ~= 200 then + error(err or ('Failed to get embeddings: ' .. (response and response.body or 'no response'))) end - local ok, content = pcall(vim.json.decode, response.body, { - luanil = { - object = true, - array = true, - }, - }) - + local ok, content = pcall(vim.json.decode, response.body) if not ok then - error('Failed to parse response: ' .. vim.inspect(content) .. '\n' .. response.body) - return {} + error('Failed to parse embedding response: ' .. response.body) end + -- Process and cache results for _, embedding in ipairs(content.data) do - table.insert(out, vim.tbl_extend('keep', chunk[embedding.index + 1], embedding)) - end - end + local result = vim.tbl_extend('keep', batch[embedding.index + 1], embedding) + table.insert(results, result) - -- Cache embeddings - for _, embedding in ipairs(out) do - if embedding.content then - local key = embedding.filename .. utils.quick_hash(embedding.content) - self.embedding_cache[key] = embedding + local cache_key = result.filename .. utils.quick_hash(result.content) + self.embedding_cache[cache_key] = result end end - -- Merge cached embeddings and newly fetched embeddings and return - return vim.list_extend(out, cached_embeddings) + return results end --- Stop the running job diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index ac6202af..3f86aa7a 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,7 +1,7 @@ local async = require('plenary.async') local log = require('plenary.log') local utils = require('CopilotChat.utils') -local tiktoken_core = nil +local _, tiktoken_core = pcall(require, 'tiktoken_core') local current_tokenizer = nil local cache_dir = vim.fn.stdpath('cache') vim.fn.mkdir(tostring(cache_dir), 'p') @@ -31,16 +31,16 @@ local M = {} --- Load the tiktoken module ---@param tokenizer string The tokenizer to load M.load = function(tokenizer) - if tokenizer == current_tokenizer then + if not tiktoken_core then return end - local ok, core = pcall(require, 'tiktoken_core') - if not ok then + if tokenizer == current_tokenizer then return end local path = load_tiktoken_data(tokenizer) + async.util.scheduler() local special_tokens = {} special_tokens['<|endoftext|>'] = 100257 special_tokens['<|fim_prefix|>'] = 100258 @@ -49,8 +49,7 @@ M.load = function(tokenizer) special_tokens['<|endofprompt|>'] = 100276 local pat_str = "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+" - core.new(path, special_tokens, pat_str) - tiktoken_core = core + tiktoken_core.new(path, special_tokens, pat_str) current_tokenizer = tokenizer end From 6ef85f63dd3a27df47e3ef22dc94f09567bdbd87 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Nov 2024 10:13:32 +0100 Subject: [PATCH 290/338] refactor(copilot): improve embedding reliability Implement adaptive batch sizing and content thresholding for embeddings to handle API failures more gracefully. This change: - Removes manual token counting in favor of API-driven feedback - Adds progressive batch size reduction when requests fail - Implements content threshold reduction for large files - Adds proper curl failure handling with --fail-with-body - Increases multi-file threshold from 3 to 5 files - Adjusts embedding thresholds for better performance --- lua/CopilotChat/context.lua | 2 +- lua/CopilotChat/copilot.lua | 157 +++++++++++++++++++----------------- 2 files changed, 83 insertions(+), 76 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 8d0fd9d4..f569b247 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -58,7 +58,7 @@ local OFF_SIDE_RULE_LANGUAGES = { local TOP_SYMBOLS = 64 local TOP_RELATED = 20 -local MULTI_FILE_THRESHOLD = 3 +local MULTI_FILE_THRESHOLD = 5 --- Compute the cosine similarity between two vectors ---@param a table diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 062500e8..ed15840f 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -28,10 +28,8 @@ local temp_file = utils.temp_file --- Constants local CONTEXT_FORMAT = '[#file:%s](#file:%s-context)' local BIG_FILE_THRESHOLD = 2000 -local BIG_EMBED_THRESHOLD = 500 +local BIG_EMBED_THRESHOLD = 300 local EMBED_MODEL = 'text-embedding-3-small' -local EMBED_MAX_TOKENS = 8191 -local EMBED_TOKENIZER = 'cl100k_base' local TRUNCATED = '... (truncated)' local TIMEOUT = 30000 local VERSION_HEADERS = { @@ -269,30 +267,28 @@ local function generate_ask_request( return out end -local function generate_embeddings_request_message(embedding) - local lines = vim.split(embedding.content, '\n') - if #lines > BIG_EMBED_THRESHOLD then - lines = vim.list_slice(lines, 1, BIG_EMBED_THRESHOLD) - table.insert(lines, TRUNCATED) - end - local content = table.concat(lines, '\n') - - if embedding.filetype == 'raw' then - return content - else - return string.format( - 'File: `%s`\n```%s\n%s\n```', - embedding.filename, - embedding.filetype, - content - ) - end -end - -local function generate_embedding_request(inputs, model) +local function generate_embedding_request(inputs, model, threshold) return { dimensions = 512, - input = vim.tbl_map(generate_embeddings_request_message, inputs), + input = vim.tbl_map(function(embedding) + local lines = vim.split(embedding.content, '\n') + if #lines > threshold then + lines = vim.list_slice(lines, 1, threshold) + table.insert(lines, TRUNCATED) + end + local content = table.concat(lines, '\n') + + if embedding.filetype == 'raw' then + return content + else + return string.format( + 'File: `%s`\n```%s\n%s\n```', + embedding.filename, + embedding.filetype, + content + ) + end + end, inputs), model = model, } end @@ -328,6 +324,8 @@ local Copilot = class(function(self, proxy, allow_insecure) proxy = proxy, insecure = allow_insecure, raw = { + -- Properly fail on errors + '--fail-with-body', -- Retry failed requests twice '--retry', '2', @@ -867,16 +865,16 @@ end --- Generate embeddings for the given inputs ---@param inputs table: The inputs to embed ---@return table -function Copilot:embed(inputs, opts) +function Copilot:embed(inputs) if not inputs or #inputs == 0 then return {} end -- Initialize essentials local model = EMBED_MODEL - tiktoken.load(EMBED_TOKENIZER) local to_process = {} local results = {} + local initial_chunk_size = 10 -- Process each input, using cache when possible for _, input in ipairs(inputs) do @@ -888,67 +886,76 @@ function Copilot:embed(inputs, opts) if self.embedding_cache[cache_key] then table.insert(results, self.embedding_cache[cache_key]) else - local message = generate_embeddings_request_message(input) - local tokens = tiktoken.count(message) - - if tokens <= EMBED_MAX_TOKENS then - input.tokens = tokens - table.insert(to_process, input) - else - log.warn( - string.format( - 'Embedding for %s exceeds token limit (%d > %d), skipping', - input.filename, - tokens, - EMBED_MAX_TOKENS - ) - ) - end + table.insert(to_process, input) end end end - -- Process inputs in batches + -- Process inputs in batches with adaptive chunk size while #to_process > 0 do - local batch = {} - local batch_tokens = 0 + local chunk_size = initial_chunk_size -- Reset chunk size for each new batch + local threshold = BIG_EMBED_THRESHOLD -- Reset threshold for each new batch - -- Build batch within token limit - while #to_process > 0 do - local next_input = to_process[1] - if batch_tokens + next_input.tokens > EMBED_MAX_TOKENS then - break - end + -- Take next chunk + local batch = {} + for _ = 1, math.min(chunk_size, #to_process) do table.insert(batch, table.remove(to_process, 1)) - batch_tokens = batch_tokens + next_input.tokens end - -- Get embeddings for batch - local body = vim.json.encode(generate_embedding_request(batch, model)) - local response, err = utils.curl_post( - 'https://api.githubcopilot.com/embeddings', - vim.tbl_extend('force', self.request_args, { - headers = self:authenticate(), - body = temp_file(body), - }) - ) + -- Try to get embeddings for batch + local success = false + local attempts = 0 + while not success and attempts < 5 do -- Limit total attempts to 5 + local body = vim.json.encode(generate_embedding_request(batch, model, threshold)) + local response, err = utils.curl_post( + 'https://api.githubcopilot.com/embeddings', + vim.tbl_extend('force', self.request_args, { + headers = self:authenticate(), + body = temp_file(body), + }) + ) - if err or not response or response.status ~= 200 then - error(err or ('Failed to get embeddings: ' .. (response and response.body or 'no response'))) - end + if err or not response or response.status ~= 200 then + attempts = attempts + 1 + -- If we have few items and the request failed, try reducing threshold first + if #batch <= 5 then + threshold = math.max(5, math.floor(threshold / 2)) + log.debug(string.format('Reducing threshold to %d and retrying...', threshold)) + else + -- Otherwise reduce batch size first + chunk_size = math.max(1, math.floor(chunk_size / 2)) + -- Put items back in to_process + for i = #batch, 1, -1 do + table.insert(to_process, 1, table.remove(batch, i)) + end + -- Take new smaller batch + batch = {} + for _ = 1, math.min(chunk_size, #to_process) do + table.insert(batch, table.remove(to_process, 1)) + end + log.debug(string.format('Reducing batch size to %d and retrying...', chunk_size)) + end + else + success = true - local ok, content = pcall(vim.json.decode, response.body) - if not ok then - error('Failed to parse embedding response: ' .. response.body) - end + -- Process and cache results + local ok, content = pcall(vim.json.decode, response.body) + if not ok then + error('Failed to parse embedding response: ' .. response.body) + end - -- Process and cache results - for _, embedding in ipairs(content.data) do - local result = vim.tbl_extend('keep', batch[embedding.index + 1], embedding) - table.insert(results, result) + for _, embedding in ipairs(content.data) do + local result = vim.tbl_extend('keep', batch[embedding.index + 1], embedding) + table.insert(results, result) + + local cache_key = result.filename .. utils.quick_hash(result.content) + self.embedding_cache[cache_key] = result + end + end + end - local cache_key = result.filename .. utils.quick_hash(result.content) - self.embedding_cache[cache_key] = result + if not success then + error('Failed to process embeddings after multiple attempts') end end From 67b7ee89c527d913c58738cffd9b21c9c8311acd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Nov 2024 12:10:54 +0100 Subject: [PATCH 291/338] fix: check if tiktoken loaded correctly Closes #631 Signed-off-by: Tomas Slusny --- lua/CopilotChat/tiktoken.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 3f86aa7a..e0cdba53 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,11 +1,15 @@ local async = require('plenary.async') local log = require('plenary.log') local utils = require('CopilotChat.utils') -local _, tiktoken_core = pcall(require, 'tiktoken_core') local current_tokenizer = nil local cache_dir = vim.fn.stdpath('cache') vim.fn.mkdir(tostring(cache_dir), 'p') +local tiktoken_ok, tiktoken_core = pcall(require, 'tiktoken_core') +if not tiktoken_ok then + tiktoken_core = nil +end + --- Load tiktoken data from cache or download it ---@param tokenizer string The tokenizer to load local function load_tiktoken_data(tokenizer) From a0b89f0af331f813d2e0f9ea9accaeb5e831356c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Nov 2024 14:17:11 +0100 Subject: [PATCH 292/338] fix: improve debug info window handling Previously, the debug info window could be opened multiple times and didn't properly handle async operations. This commit: - Makes buffer parameter optional in context.buffer() - Adds proper window cleanup on close - Wraps debug info building in async operation to prevent UI blocking - Stores window number reference to manage single instance --- lua/CopilotChat/context.lua | 3 +- lua/CopilotChat/ui/debug.lua | 78 ++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index f569b247..0099e1a3 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -358,10 +358,11 @@ function M.file(filename) end --- Get the content of a buffer ----@param bufnr number +---@param bufnr? number ---@return CopilotChat.copilot.embed? function M.buffer(bufnr) async.util.scheduler() + bufnr = bufnr or vim.api.nvim_get_current_buf() if not utils.buf_valid(bufnr) then return nil diff --git a/lua/CopilotChat/ui/debug.lua b/lua/CopilotChat/ui/debug.lua index 10f6ab30..019ac55f 100644 --- a/lua/CopilotChat/ui/debug.lua +++ b/lua/CopilotChat/ui/debug.lua @@ -1,3 +1,4 @@ +local async = require('plenary.async') local log = require('plenary.log') local utils = require('CopilotChat.utils') local context = require('CopilotChat.context') @@ -23,7 +24,7 @@ local function build_debug_info() '', } - local buf = context.buffer(vim.api.nvim_get_current_buf()) + local buf = context.buffer() local outline = buf and context.outline(buf.content, buf.filename, buf.filetype) if outline then table.insert(lines, 'Current buffer symbols:') @@ -76,42 +77,59 @@ local Debug = class(function(self) end) end, Overlay) +function Debug:close() + if not self.winnr then + return + end + + if vim.api.nvim_win_is_valid(self.winnr) then + vim.api.nvim_win_close(self.winnr, true) + end + + self.winnr = nil +end + function Debug:open() self:validate() + self:close() - local lines = build_debug_info() - local height = math.min(vim.o.lines - 3, #lines) - local width = 0 - for _, line in ipairs(lines) do - width = math.max(width, #line) - end + async.run(function() + local lines = build_debug_info() + async.util.scheduler() - local win_opts = { - title = 'CopilotChat.nvim Debug Info', - relative = 'editor', - width = width, - height = height, - row = math.floor((vim.o.lines - height) / 2) - 1, - col = math.floor((vim.o.columns - width) / 2), - style = 'minimal', - border = 'rounded', - zindex = 50, - } + local height = math.min(vim.o.lines - 3, #lines) + local width = 0 + for _, line in ipairs(lines) do + width = math.max(width, #line) + end - if not utils.is_stable() then - win_opts.footer = "Press 'q' to close this window." - end + local win_opts = { + title = 'CopilotChat.nvim Debug Info', + relative = 'editor', + width = width, + height = height, + row = math.floor((vim.o.lines - height) / 2) - 1, + col = math.floor((vim.o.columns - width) / 2), + style = 'minimal', + border = 'rounded', + zindex = 50, + } - -- Open window - local winnr = vim.api.nvim_open_win(self.bufnr, true, win_opts) - vim.wo[winnr].wrap = true - vim.wo[winnr].linebreak = true - vim.wo[winnr].cursorline = true - vim.wo[winnr].conceallevel = 2 + if not utils.is_stable() then + win_opts.footer = "Press 'q' to close this window." + end + + -- Open window + self.winnr = vim.api.nvim_open_win(self.bufnr, true, win_opts) + vim.wo[self.winnr].wrap = true + vim.wo[self.winnr].linebreak = true + vim.wo[self.winnr].cursorline = true + vim.wo[self.winnr].conceallevel = 2 - -- Show content - self:show(table.concat(lines, '\n'), winnr, 'markdown') - vim.api.nvim_win_set_cursor(winnr, { 1, 0 }) + -- Show content + self:show(table.concat(lines, '\n'), self.winnr, 'markdown') + vim.api.nvim_win_set_cursor(self.winnr, { 1, 0 }) + end) end return Debug From e0c4ca0e7e1801452586ba03662ec5ba61f4f55e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Nov 2024 21:32:57 +0100 Subject: [PATCH 293/338] refactor(context): optimize file loading with caching Refactor context.lua to introduce file caching mechanism and improve code organization around file handling. The main changes include: - Add file caching to avoid reprocessing unchanged files - Move outline generation logic into separate build_outline function - Consolidate embed type definitions into context.lua - Remove duplicate type definitions from copilot.lua - Optimize file loading with new get_file helper function Signed-off-by: Tomas Slusny --- README.md | 2 +- lua/CopilotChat/config.lua | 19 +---- lua/CopilotChat/context.lua | 137 +++++++++++++++++++---------------- lua/CopilotChat/copilot.lua | 24 ++---- lua/CopilotChat/init.lua | 4 +- lua/CopilotChat/select.lua | 25 +++++-- lua/CopilotChat/ui/debug.lua | 11 ++- lua/CopilotChat/utils.lua | 11 +++ 8 files changed, 124 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index bdc5a622..3969f906 100644 --- a/README.md +++ b/README.md @@ -719,7 +719,7 @@ require('CopilotChat').setup({ ## Roadmap (Wishlist) -- Caching for contexts +- Improved caching for context (persistence through restarts/smarter caching) - General QOL improvements ## Development diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index f92edbd3..98e7bca0 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -7,25 +7,10 @@ local utils = require('CopilotChat.utils') --- @field bufnr number --- @field winnr number ----@class CopilotChat.config.selection.diagnostic ----@field content string ----@field start_line number ----@field end_line number ----@field severity string - ----@class CopilotChat.config.selection ----@field content string ----@field start_line number ----@field end_line number ----@field filename string ----@field filetype string ----@field bufnr number ----@field diagnostics table? - ---@class CopilotChat.config.context ---@field description string? ---@field input fun(callback: fun(input: string?), source: CopilotChat.config.source)? ----@field resolve fun(input: string?, source: CopilotChat.config.source):table +---@field resolve fun(input: string?, source: CopilotChat.config.source):table ---@class CopilotChat.config.prompt : CopilotChat.config.shared ---@field prompt string? @@ -76,7 +61,7 @@ local utils = require('CopilotChat.utils') ---@field temperature number? ---@field headless boolean? ---@field callback fun(response: string, source: CopilotChat.config.source)? ----@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.config.selection? +---@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.select.selection? ---@field window CopilotChat.config.window? ---@field show_help boolean? ---@field show_folds boolean? diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 0099e1a3..d55449cc 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -7,12 +7,18 @@ ---@field end_row number ---@field end_col number ----@class CopilotChat.context.outline : CopilotChat.copilot.embed ----@field symbols table +---@class CopilotChat.context.embed +---@field content string +---@field filename string +---@field filetype string +---@field original string? +---@field symbols table? +---@field embedding table? local async = require('plenary.async') local log = require('plenary.log') local utils = require('CopilotChat.utils') +local file_cache = {} local M = {} @@ -79,10 +85,10 @@ local function spatial_distance_cosine(a, b) end --- Rank data by relatedness to the query ----@param query CopilotChat.copilot.embed ----@param data table +---@param query CopilotChat.context.embed +---@param data table ---@param top_n number ----@return table +---@return table local function data_ranked_by_relatedness(query, data, top_n) data = vim.tbl_map(function(item) return vim.tbl_extend( @@ -101,7 +107,7 @@ end --- Rank data by symbols ---@param query string ----@param data table +---@param data table ---@param top_n number local function data_ranked_by_symbols(query, data, top_n) local query_terms = {} @@ -193,22 +199,15 @@ end --- Build an outline and symbols from a string ---@param content string ---@param filename string ----@param ft string? ----@return CopilotChat.context.outline -function M.outline(content, filename, ft) - ft = ft or 'text' - +---@param ft string +---@return CopilotChat.context.embed +local function build_outline(content, filename, ft) local output = { filename = filename, filetype = ft, content = content, - symbols = {}, } - if ft == 'raw' then - return output - end - local lang = vim.treesitter.language.get_lang(ft) local ok, parser = false, nil if lang then @@ -224,6 +223,7 @@ function M.outline(content, filename, ft) local root = parser:parse()[1]:root() local lines = vim.split(content, '\n') + local symbols = {} local outline_lines = {} local depth = 0 @@ -239,7 +239,7 @@ function M.outline(content, filename, ft) table.insert(outline_lines, string.rep(' ', depth) .. signature_start) -- Store symbol information - table.insert(output.symbols, { + table.insert(symbols, { name = name, signature = signature_start, type = type, @@ -269,15 +269,45 @@ function M.outline(content, filename, ft) if #outline_lines > 0 then output.original = content output.content = table.concat(outline_lines, '\n') + output.symbols = symbols end return output end +--- Get data for a file +---@param filename string +---@param filetype string +---@return CopilotChat.context.embed? +local function get_file(filename, filetype) + local modified = utils.file_mtime(filename) + if not modified then + return nil + end + + local cached = file_cache[filename] + if cached and cached.modified >= modified then + return cached.outline + end + + local content = utils.read_file(filename) + if content then + local outline = build_outline(content, filename, filetype) + file_cache[filename] = { + outline = outline, + modified = modified, + } + + return outline + end + + return nil +end + --- Get list of all files in workspace ---@param winnr number? ---@param with_content boolean? ----@return table +---@return table function M.files(winnr, with_content) local cwd = utils.win_cwd(winnr) local files = utils.scan_dir(cwd, { @@ -291,24 +321,22 @@ function M.files(winnr, with_content) if with_content then async.util.scheduler() - files = vim.tbl_map(function(file) - return { - name = utils.filepath(file), - ft = utils.filetype(file), - } - end, files) - files = vim.tbl_filter(function(file) - return file.ft ~= nil - end, files) + files = vim.tbl_filter( + function(file) + return file.ft ~= nil + end, + vim.tbl_map(function(file) + return { + name = utils.filepath(file), + ft = utils.filetype(file), + } + end, files) + ) for _, file in ipairs(files) do - local content = utils.read_file(file.name) - if content then - table.insert(out, { - content = content, - filename = file.name, - filetype = file.ft, - }) + local file_data = get_file(file.name, file.ft) + if file_data then + table.insert(out, file_data) end end @@ -338,28 +366,20 @@ end --- Get the content of a file ---@param filename string ----@return CopilotChat.copilot.embed? +---@return CopilotChat.context.embed? function M.file(filename) - local content = utils.read_file(filename) - if not content then - return nil - end - async.util.scheduler() - if not utils.filetype(filename) then + local ft = utils.filetype(filename) + if not ft then return nil end - return { - content = content, - filename = utils.filepath(filename), - filetype = utils.filetype(filename), - } + return get_file(utils.filepath(filename), ft) end --- Get the content of a buffer ---@param bufnr? number ----@return CopilotChat.copilot.embed? +---@return CopilotChat.context.embed? function M.buffer(bufnr) async.util.scheduler() bufnr = bufnr or vim.api.nvim_get_current_buf() @@ -373,17 +393,17 @@ function M.buffer(bufnr) return nil end - return { - content = table.concat(content, '\n'), - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), - filetype = vim.bo[bufnr].filetype, - } + return build_outline( + table.concat(content, '\n'), + utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + vim.bo[bufnr].filetype + ) end --- Get current git diff ---@param type string? ---@param winnr number ----@return CopilotChat.copilot.embed? +---@return CopilotChat.context.embed? function M.gitdiff(type, winnr) type = type or 'unstaged' local cwd = utils.win_cwd(winnr) @@ -411,7 +431,7 @@ end --- Return contents of specified register ---@param register string? ----@return CopilotChat.copilot.embed? +---@return CopilotChat.context.embed? function M.register(register) register = register or '+' local lines = vim.fn.getreg(register) @@ -429,19 +449,14 @@ end --- Filter embeddings based on the query ---@param copilot CopilotChat.Copilot ---@param prompt string ----@param embeddings table ----@return table +---@param embeddings table +---@return table function M.filter_embeddings(copilot, prompt, embeddings) -- If we dont need to embed anything, just return directly if #embeddings < MULTI_FILE_THRESHOLD then return embeddings end - -- Map embeddings to outlines - embeddings = vim.tbl_map(function(embed) - return M.outline(embed.content, embed.filename, embed.filetype) - end, embeddings) - -- Rank embeddings by symbols embeddings = data_ranked_by_symbols(prompt, embeddings, TOP_SYMBOLS) log.debug('Ranked data:', #embeddings) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index ed15840f..f1821ee3 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -1,12 +1,6 @@ ----@class CopilotChat.copilot.embed ----@field content string ----@field filename string ----@field filetype string ----@field embedding table - ---@class CopilotChat.copilot.ask.opts ----@field selection CopilotChat.config.selection? ----@field embeddings table? +---@field selection CopilotChat.select.selection? +---@field embeddings table? ---@field system_prompt string? ---@field model string? ---@field agent string? @@ -14,10 +8,6 @@ ---@field no_history boolean? ---@field on_progress nil|fun(response: string):nil ----@class CopilotChat.copilot.embed.opts ----@field model string? ----@field chunk_size number? - local log = require('plenary.log') local prompts = require('CopilotChat.prompts') local tiktoken = require('CopilotChat.tiktoken') @@ -107,7 +97,7 @@ local function generate_line_numbers(content, start_line) end --- Generate messages for the given selection ---- @param selection CopilotChat.config.selection +--- @param selection CopilotChat.select.selection local function generate_selection_messages(selection) local filename = selection.filename or 'unknown' local filetype = selection.filetype or 'text' @@ -167,7 +157,7 @@ local function generate_selection_messages(selection) end --- Generate messages for the given embeddings ---- @param embeddings table +--- @param embeddings table local function generate_embeddings_messages(embeddings) local files = {} for _, embedding in ipairs(embeddings) do @@ -295,7 +285,7 @@ end ---@class CopilotChat.Copilot : Class ---@field history table ----@field embedding_cache table +---@field embedding_cache table ---@field policies table ---@field models table? ---@field agents table? @@ -863,8 +853,8 @@ function Copilot:list_agents() end --- Generate embeddings for the given inputs ----@param inputs table: The inputs to embed ----@return table +---@param inputs table: The inputs to embed +---@return table function Copilot:embed(inputs) if not inputs or #inputs == 0 then return {} diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 83cd38e9..9c00d0b0 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -42,7 +42,7 @@ local state = { } ---@param config CopilotChat.config.shared ----@return CopilotChat.config.selection? +---@return CopilotChat.select.selection? local function get_selection(config) local bufnr = state.source and state.source.bufnr local winnr = state.source and state.source.winnr @@ -231,7 +231,7 @@ end ---@param prompt string ---@param config CopilotChat.config.shared ----@return table, string +---@return table, string local function resolve_embeddings(prompt, config) local contexts = {} local function parse_context(prompt_context) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 37a72bd7..67f5ecf1 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -1,3 +1,18 @@ +---@class CopilotChat.select.selection.diagnostic +---@field content string +---@field start_line number +---@field end_line number +---@field severity string + +---@class CopilotChat.select.selection +---@field content string +---@field start_line number +---@field end_line number +---@field filename string +---@field filetype string +---@field bufnr number +---@field diagnostics table? + local utils = require('CopilotChat.utils') local M = {} @@ -6,7 +21,7 @@ local M = {} --- @param bufnr number --- @param start_line number --- @param end_line number ---- @return table|nil +--- @return table|nil local function get_diagnostics_in_range(bufnr, start_line, end_line) local diagnostics = vim.diagnostic.get(bufnr) local range_diagnostics = {} @@ -34,7 +49,7 @@ end --- Select and process current visual selection --- @param source CopilotChat.config.source ---- @return CopilotChat.config.selection|nil +--- @return CopilotChat.select.selection|nil function M.visual(source) local bufnr = source.bufnr local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '<')) @@ -68,7 +83,7 @@ end --- Select and process whole buffer --- @param source CopilotChat.config.source ---- @return CopilotChat.config.selection|nil +--- @return CopilotChat.select.selection|nil function M.buffer(source) local bufnr = source.bufnr local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) @@ -91,7 +106,7 @@ end --- Select and process current line --- @param source CopilotChat.config.source ---- @return CopilotChat.config.selection|nil +--- @return CopilotChat.select.selection|nil function M.line(source) local bufnr = source.bufnr local winnr = source.winnr @@ -116,7 +131,7 @@ end --- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. --- @param source CopilotChat.config.source ---- @return CopilotChat.config.selection|nil +--- @return CopilotChat.select.selection|nil function M.unnamed(source) local bufnr = source.bufnr local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '[')) diff --git a/lua/CopilotChat/ui/debug.lua b/lua/CopilotChat/ui/debug.lua index 019ac55f..42ceb956 100644 --- a/lua/CopilotChat/ui/debug.lua +++ b/lua/CopilotChat/ui/debug.lua @@ -25,10 +25,9 @@ local function build_debug_info() } local buf = context.buffer() - local outline = buf and context.outline(buf.content, buf.filename, buf.filetype) - if outline then + if buf then table.insert(lines, 'Current buffer symbols:') - for _, symbol in ipairs(outline.symbols) do + for _, symbol in ipairs(buf.symbols) do table.insert( lines, string.format( @@ -44,9 +43,9 @@ local function build_debug_info() ) end table.insert(lines, 'Current buffer outline:') - table.insert(lines, '`' .. outline.filename .. '`') - table.insert(lines, '```' .. outline.filetype) - local outline_lines = vim.split(outline.content, '\n') + table.insert(lines, '`' .. buf.filename .. '`') + table.insert(lines, '```' .. buf.filetype) + local outline_lines = vim.split(buf.content, '\n') for _, line in ipairs(outline_lines) do table.insert(lines, line) end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 64349bf9..70ae0f34 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -301,6 +301,17 @@ M.file_exists = function(path) return err == nil and stat ~= nil end +--- Get last modified time of a file +---@param path string The file path +---@return number? +M.file_mtime = function(path) + local err, stat = async.uv.fs_stat(path) + if err or not stat then + return nil + end + return stat.mtime.sec +end + --- Read a file ---@param path string The file path M.read_file = function(path) From 1ff48196f849af979e85c418d6ad5ded1740ad1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Nov 2024 20:40:55 +0000 Subject: [PATCH 294/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ff83a160..1131481f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -757,7 +757,7 @@ installed. ROADMAP (WISHLIST) *CopilotChat-roadmap-(wishlist)* -- Caching for contexts +- Improved caching for context (persistence through restarts/smarter caching) - General QOL improvements From f2fc7cafc54de58718ea8d769e457d864f013fb0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 28 Nov 2024 22:24:08 +0100 Subject: [PATCH 295/338] refactor: simplify buffer handling using builtin functions Replace manual buffer search and creation logic with built-in Neovim functions bufadd() and bufload(). This change makes the code more concise and reliable by leveraging Neovim's native buffer management APIs. The new implementation: - Removes redundant buffer search loop - Uses bufadd() to add new buffer - Uses bufload() to ensure buffer is loaded Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9c00d0b0..0ceee510 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -171,7 +171,7 @@ end ---@param end_line number ---@param config CopilotChat.config.shared local function jump_to_diff(winnr, bufnr, start_line, end_line, config) - vim.api.nvim_win_set_cursor(winnr, { start_line, 0 }) + pcall(vim.api.nvim_win_set_cursor, winnr, { start_line, 0 }) pcall(vim.api.nvim_buf_set_mark, bufnr, '<', start_line, 0, {}) pcall(vim.api.nvim_buf_set_mark, bufnr, '>', end_line, 0, {}) pcall(vim.api.nvim_buf_set_mark, bufnr, '[', start_line, 0, {}) @@ -1062,24 +1062,15 @@ function M.setup(config) local diff_bufnr = diff.bufnr - -- Try to find existing buffer first + -- If buffer is not found, try to load it if not diff_bufnr then - for _, buf in ipairs(vim.api.nvim_list_bufs()) do - if utils.filename_same(vim.api.nvim_buf_get_name(buf), diff.filename) then - diff_bufnr = buf - break - end - end - end - - -- Create new empty buffer if doesn't exist - if not diff_bufnr then - diff_bufnr = vim.api.nvim_create_buf(true, false) - vim.api.nvim_buf_set_name(diff_bufnr, diff.filename) - vim.bo[diff_bufnr].filetype = diff.filetype + diff_bufnr = vim.fn.bufadd(diff.filename) + vim.fn.bufload(diff_bufnr) end + state.source.bufnr = diff_bufnr vim.api.nvim_win_set_buf(state.source.winnr, diff_bufnr) + jump_to_diff( state.source.winnr, diff_bufnr, From 41895c519b239e661c80ade301de3be95eff6161 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 00:52:07 +0100 Subject: [PATCH 296/338] fix: improve logging output format and consistency Replace vim.inspect usage with new make_string utility function to provide more consistent and cleaner logging output across the codebase. This makes logs more readable and uniform when handling both simple values and complex data structures. The changes include: - Add make_string utility function for standardized string conversion - Replace vim.inspect calls with direct logging or make_string - Improve error message formatting for better readability Signed-off-by: Tomas Slusny --- lua/CopilotChat/copilot.lua | 38 ++++++++++++++++++------------------- lua/CopilotChat/init.lua | 6 +++--- lua/CopilotChat/utils.lua | 27 ++++++++++++++++++++------ 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index f1821ee3..6deb0c3b 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -427,7 +427,7 @@ function Copilot:fetch_models() end log.info('Models fetched') - log.trace(vim.inspect(models)) + log.trace(models) self.models = out return out end @@ -463,7 +463,7 @@ function Copilot:fetch_agents() out['copilot'] = { name = 'Copilot', default = true, description = 'Default noop agent' } log.info('Agents fetched') - log.trace(vim.inspect(agents)) + log.trace(agents) self.agents = out return out end @@ -486,7 +486,7 @@ function Copilot:enable_policy(model) self.policies[model] = true if err or response.status ~= 200 then - log.warn('Failed to enable policy for ' .. model .. ': ' .. vim.inspect(err or response.body)) + log.warn('Failed to enable policy for ', model, ': ', (err or response.body)) return end @@ -510,13 +510,13 @@ function Copilot:ask(prompt, opts) local job_id = utils.uuid() self.current_job = job_id - log.trace('System prompt: ' .. system_prompt) - log.trace('Selection: ' .. (selection.content or '')) - log.debug('Prompt: ' .. prompt) - log.debug('Embeddings: ' .. #embeddings) - log.debug('Model: ' .. model) - log.debug('Agent: ' .. agent) - log.debug('Temperature: ' .. temperature) + log.trace('System prompt: ', system_prompt) + log.trace('Selection: ', selection.content) + log.debug('Prompt: ', prompt) + log.debug('Embeddings: ', #embeddings) + log.debug('Model: ', model) + log.debug('Agent: ', agent) + log.debug('Temperature: ', temperature) local history = no_history and {} or self.history local models = self:fetch_models() @@ -534,8 +534,8 @@ function Copilot:ask(prompt, opts) local max_tokens = capabilities.limits.max_prompt_tokens -- FIXME: Is max_prompt_tokens the right limit? local max_output_tokens = capabilities.limits.max_output_tokens local tokenizer = capabilities.tokenizer - log.debug('Max tokens: ' .. max_tokens) - log.debug('Tokenizer: ' .. tokenizer) + log.debug('Max tokens: ', max_tokens) + log.debug('Tokenizer: ', tokenizer) tiktoken.load(tokenizer) local generated_messages = {} @@ -666,7 +666,7 @@ function Copilot:ask(prompt, opts) local err = parse_line(line) if err and job then - finish_stream('Failed to parse response: ' .. vim.inspect(err) .. '\n' .. line, job) + finish_stream('Failed to parse response: ' .. utils.make_string(err) .. '\n' .. line, job) end end @@ -681,7 +681,7 @@ function Copilot:ask(prompt, opts) end if err then - finish_stream('Failed to get response: ' .. (err and vim.inspect(err) or line), job) + finish_stream('Failed to get response: ' .. utils.make_string(err and err or line), job) return end @@ -735,9 +735,9 @@ function Copilot:ask(prompt, opts) return end - log.debug('Response status: ' .. response.status) - log.debug('Response body: ' .. response.body) - log.debug('Response headers: ' .. vim.inspect(response.headers)) + log.debug('Response status: ', response.status) + log.debug('Response body: ', response.body) + log.debug('Response headers: ', response.headers) if response.status ~= 200 then if response.status == 401 then @@ -791,8 +791,8 @@ function Copilot:ask(prompt, opts) end end - log.trace('Full response: ' .. full_response) - log.debug('Last message: ' .. vim.inspect(last_message)) + log.trace('Full response: ', full_response) + log.debug('Last message: ', last_message) table.insert(history, { content = prompt, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0ceee510..302a4c49 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -317,7 +317,7 @@ local function show_error(err, append_newline) message = message:gsub('^%s*', '') err = message else - err = vim.inspect(err) + err = utils.make_string(err) end if append_newline then @@ -713,7 +713,7 @@ function M.ask(prompt, config) if not query_ok then async.util.scheduler() - log.error(vim.inspect(filtered_embeddings)) + log.error(filtered_embeddings) if not config.headless then show_error(filtered_embeddings, has_output) end @@ -740,7 +740,7 @@ function M.ask(prompt, config) async.util.scheduler() if not ask_ok then - log.error(vim.inspect(response)) + log.error(response) if not config.headless then show_error(response, has_output) end diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 70ae0f34..caa42579 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -230,6 +230,25 @@ function M.quick_hash(str) return #str .. str:sub(1, 32) .. str:sub(-32) end +--- Make a string from arguments +---@vararg any The arguments +---@return string +function M.make_string(...) + local t = {} + for i = 1, select('#', ...) do + local x = select(i, ...) + + if type(x) == 'table' then + x = vim.inspect(x) + else + x = tostring(x) + end + + t[#t + 1] = x + end + return table.concat(t, ' ') +end + --- Get current working directory for target window ---@param winnr number? The buffer number ---@return string @@ -255,7 +274,7 @@ M.curl_get = async.wrap(function(url, opts, callback) vim.tbl_deep_extend('force', opts, { callback = callback, on_error = function(err) - err = err and err.stderr or vim.inspect(err) + err = M.make_string(err and err.stderr or err) callback(nil, err) end, }) @@ -271,7 +290,7 @@ M.curl_post = async.wrap(function(url, opts, callback) vim.tbl_deep_extend('force', opts, { callback = callback, on_error = function(err) - err = err and err.stderr or vim.inspect(err) + err = M.make_string(err and err.stderr or err) callback(nil, err) end, }) @@ -286,10 +305,6 @@ M.scan_dir = async.wrap(function(path, opts, callback) path, vim.tbl_deep_extend('force', opts, { on_exit = callback, - on_error = function(err) - err = err and err.stderr or vim.inspect(err) - callback(nil, err) - end, }) ) end, 3) From e475d54863222bccc4c9fafffb573d8ee45c34e9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 02:11:33 +0100 Subject: [PATCH 297/338] fix: add error handling to chat async operation Previously errors in the async chat operation were not properly caught and handled, which could lead to silent failures. This change adds proper error handling using pcall and displays errors to users when not in headless mode. --- lua/CopilotChat/init.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 302a4c49..362c5ba1 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -683,7 +683,7 @@ function M.ask(prompt, config) -- Retrieve the selection local selection = get_selection(config) - async.run(function() + local ok, err = pcall(async.run, function() local embeddings, embedded_prompt = resolve_embeddings(prompt, config) prompt = embedded_prompt @@ -764,6 +764,13 @@ function M.ask(prompt, config) config.callback(response, state.source) end end) + + if not ok then + log.error(err) + if not config.headless then + show_error(err) + end + end end --- Stop current copilot output and optionally reset the chat ten show the help message. From d261d5196b5edb38f39fc8b7b59846007f388178 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 Nov 2024 01:12:26 +0000 Subject: [PATCH 298/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 1131481f..8805a39f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 28 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 29 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From b054b9f63fd7190dd0bffbe6ef236e5d75b6365a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 08:32:25 +0100 Subject: [PATCH 299/338] refactor: unify info display into show_info and show_context Consolidates multiple info display commands (show_system_prompt, show_user_selection, show_user_context) into two more comprehensive commands: - show_info (gi): displays model, agent and system prompt - show_context (gc): shows selection and embeddings This change improves UX by providing more organized and complete information when inspecting chat context and configuration. BREAKING CHANGE: Removed show_system_prompt (gp), show_user_selection (gs) and show_user_context mappings in favor of new unified commands. Signed-off-by: Tomas Slusny --- README.md | 14 ++-- lua/CopilotChat/config.lua | 14 ++-- lua/CopilotChat/init.lua | 156 ++++++++++++++++++++++++++----------- 3 files changed, 122 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 3969f906..8b0e2092 100644 --- a/README.md +++ b/README.md @@ -148,9 +148,8 @@ See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/ma - `gq` - Add all diffs from chat to quickfix list - `gy` - Yank nearest diff to register (defaults to `"`) - `gd` - Show diff between source and nearest diff -- `gp` - Show system prompt for current chat -- `gs` - Show current user selection -- `gc` - Show current user context +- `gi` - Show info about current chat (model, agent, system prompt) +- `gc` - Show current chat context - `gh` - Show help message The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: @@ -562,13 +561,10 @@ Also see [here](/lua/CopilotChat/config.lua): show_diff = { normal = 'gd', }, - show_system_prompt = { - normal = 'gp', + show_info = { + normal = 'gi', }, - show_user_selection = { - normal = 'gs', - }, - show_user_context = { + show_context = { normal = 'gc', }, show_help = { diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 98e7bca0..f413b906 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -48,9 +48,8 @@ local utils = require('CopilotChat.utils') ---@field quickfix_diffs CopilotChat.config.mapping? ---@field yank_diff CopilotChat.config.mapping.register? ---@field show_diff CopilotChat.config.mapping? ----@field show_system_prompt CopilotChat.config.mapping? ----@field show_user_selection CopilotChat.config.mapping? ----@field show_user_context CopilotChat.config.mapping? +---@field show_info CopilotChat.config.mapping? +---@field show_context CopilotChat.config.mapping? ---@field show_help CopilotChat.config.mapping? ---@class CopilotChat.config.shared @@ -378,13 +377,10 @@ return { show_diff = { normal = 'gd', }, - show_system_prompt = { - normal = 'gp', + show_info = { + normal = 'gi', }, - show_user_selection = { - normal = 'gs', - }, - show_user_context = { + show_context = { normal = 'gc', }, show_help = { diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 362c5ba1..5c49311b 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -281,6 +281,34 @@ local function resolve_embeddings(prompt, config) return embeddings:values(), prompt end +local function resolve_agent(prompt, config) + local agents = vim.tbl_keys(state.copilot:list_agents()) + local selected_agent = config.agent + prompt = prompt:gsub('@' .. WORD, function(match) + if vim.tbl_contains(agents, match) then + selected_agent = match + return '' + end + return '@' .. match + end) + + return selected_agent, prompt +end + +local function resolve_model(prompt, config) + local models = vim.tbl_keys(state.copilot:list_models()) + local selected_model = config.model + prompt = prompt:gsub('%$' .. WORD, function(match) + if vim.tbl_contains(models, match) then + selected_model = match + return '' + end + return '$' .. match + end) + + return selected_model, prompt +end + ---@param start_of_chat boolean? local function finish(start_of_chat) if not start_of_chat then @@ -684,28 +712,9 @@ function M.ask(prompt, config) local selection = get_selection(config) local ok, err = pcall(async.run, function() - local embeddings, embedded_prompt = resolve_embeddings(prompt, config) - prompt = embedded_prompt - - local agents = vim.tbl_keys(state.copilot:list_agents()) - local selected_agent = config.agent - prompt = prompt:gsub('@' .. WORD, function(match) - if vim.tbl_contains(agents, match) then - selected_agent = match - return '' - end - return '@' .. match - end) - - local models = vim.tbl_keys(state.copilot:list_models()) - local selected_model = config.model - prompt = prompt:gsub('%$' .. WORD, function(match) - if vim.tbl_contains(models, match) then - selected_model = match - return '' - end - return '$' .. match - end) + local embeddings, prompt = resolve_embeddings(prompt, config) + local selected_agent, prompt = resolve_agent(prompt, config) + local selected_model, prompt = resolve_model(prompt, config) local has_output = false local query_ok, filtered_embeddings = @@ -875,6 +884,14 @@ function M.setup(config) normal = key, } end + + if name == 'show_system_prompt' then + utils.deprecate('config.mappings.' .. name, 'config.mappings.show_info') + end + + if name == 'show_user_context' or name == 'show_user_selection' then + utils.deprecate('config.mappings.' .. name, 'config.mappings.show_context') + end end end @@ -1137,30 +1154,73 @@ function M.setup(config) state.diff:show(diff, state.chat.winnr) end) - map_key('show_system_prompt', bufnr, function() + map_key('show_info', bufnr, function() local section = state.chat:get_closest_section() - local system_prompt = state.chat.config.system_prompt - if section and not section.answer then - _, system_prompt = resolve_prompts(section.content, system_prompt) - end - if not system_prompt then + if not section or section.answer then return end - state.overlay:show(vim.trim(system_prompt) .. '\n', state.chat.winnr, 'markdown') + local lines = {} + local config = state.chat.config + local prompt, system_prompt = resolve_prompts(section.content, config.system_prompt) + + async.run(function() + local selected_agent = resolve_agent(prompt, config) + local selected_model = resolve_model(prompt, config) + + if selected_model then + table.insert(lines, '**Model**') + table.insert(lines, '```') + table.insert(lines, selected_model) + table.insert(lines, '```') + table.insert(lines, '') + end + + if selected_agent then + table.insert(lines, '**Agent**') + table.insert(lines, '```') + table.insert(lines, selected_agent) + table.insert(lines, '```') + table.insert(lines, '') + end + + if system_prompt then + table.insert(lines, '**System Prompt**') + table.insert(lines, '```') + for _, line in ipairs(vim.split(vim.trim(system_prompt), '\n')) do + table.insert(lines, line) + end + table.insert(lines, '```') + table.insert(lines, '') + end + + async.util.scheduler() + state.overlay:show( + vim.trim(table.concat(lines, '\n')) .. '\n', + state.chat.winnr, + 'markdown' + ) + end) end) - map_key('show_user_selection', bufnr, function() - local selection = get_selection(state.chat.config) - if not selection then + map_key('show_context', bufnr, function() + local section = state.chat:get_closest_section() + if not section or section.answer then return end - state.overlay:show(selection.content, state.chat.winnr, selection.filetype) - end) + local lines = {} - map_key('show_user_context', bufnr, function() - local section = state.chat:get_closest_section() + local selection = get_selection(state.chat.config) + if selection then + table.insert(lines, '**Selection**') + table.insert(lines, '```' .. selection.filetype) + for _, line in ipairs(vim.split(selection.content, '\n')) do + table.insert(lines, line) + end + table.insert(lines, '```') + table.insert(lines, '') + end async.run(function() local embeddings = {} @@ -1168,21 +1228,29 @@ function M.setup(config) embeddings = resolve_embeddings(section.content, state.chat.config) end - local text = '' for _, embedding in ipairs(embeddings) do - local lines = vim.split(embedding.content, '\n') - local preview = table.concat(vim.list_slice(lines, 1, math.min(10, #lines)), '\n') - local header = string.format('**`%s`** (%s lines)', embedding.filename, #lines) - if #lines > 10 then + local embed_lines = vim.split(embedding.content, '\n') + local preview = vim.list_slice(embed_lines, 1, math.min(10, #embed_lines)) + local header = string.format('**%s** (%s lines)', embedding.filename, #embed_lines) + if #embed_lines > 10 then header = header .. ' (truncated)' end - text = text - .. string.format('%s\n```%s\n%s\n```\n\n', header, embedding.filetype, preview) + table.insert(lines, header) + table.insert(lines, '```' .. embedding.filetype) + for _, line in ipairs(preview) do + table.insert(lines, line) + end + table.insert(lines, '```') + table.insert(lines, '') end async.util.scheduler() - state.overlay:show(vim.trim(text) .. '\n', state.chat.winnr, 'markdown') + state.overlay:show( + vim.trim(table.concat(lines, '\n')) .. '\n', + state.chat.winnr, + 'markdown' + ) end) end) From b18cdebfa5ec7d8f04d79ac1aeaf8c9586838b68 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 Nov 2024 07:52:33 +0000 Subject: [PATCH 300/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 8805a39f..126135b6 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -176,9 +176,8 @@ CHAT MAPPINGS ~ - `gq` - Add all diffs from chat to quickfix list - `gy` - Yank nearest diff to register (defaults to `"`) - `gd` - Show diff between source and nearest diff -- `gp` - Show system prompt for current chat -- `gs` - Show current user selection -- `gc` - Show current user context +- `gi` - Show info about current chat (model, agent, system prompt) +- `gc` - Show current chat context - `gh` - Show help message The mappings can be customized by setting the `mappings` table in your @@ -613,13 +612,10 @@ Also see here : show_diff = { normal = 'gd', }, - show_system_prompt = { - normal = 'gp', + show_info = { + normal = 'gi', }, - show_user_selection = { - normal = 'gs', - }, - show_user_context = { + show_context = { normal = 'gc', }, show_help = { From a127418df03abeb132ea779dff7cc1684f62b4cf Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 09:19:23 +0100 Subject: [PATCH 301/338] fix: add null check for buffer symbols Add a null check for buf.symbols before attempting to iterate through the symbols array in debug info builder. This prevents potential errors when symbols are not available for the current buffer. --- lua/CopilotChat/ui/debug.lua | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lua/CopilotChat/ui/debug.lua b/lua/CopilotChat/ui/debug.lua index 42ceb956..4ab9ae81 100644 --- a/lua/CopilotChat/ui/debug.lua +++ b/lua/CopilotChat/ui/debug.lua @@ -26,21 +26,23 @@ local function build_debug_info() local buf = context.buffer() if buf then - table.insert(lines, 'Current buffer symbols:') - for _, symbol in ipairs(buf.symbols) do - table.insert( - lines, - string.format( - '%s `%s` (%s %s %s %s) - `%s`', - symbol.type, - symbol.name, - symbol.start_row, - symbol.start_col, - symbol.end_row, - symbol.end_col, - symbol.signature + if buf.symbols then + table.insert(lines, 'Current buffer symbols:') + for _, symbol in ipairs(buf.symbols) do + table.insert( + lines, + string.format( + '%s `%s` (%s %s %s %s) - `%s`', + symbol.type, + symbol.name, + symbol.start_row, + symbol.start_col, + symbol.end_row, + symbol.end_col, + symbol.signature + ) ) - ) + end end table.insert(lines, 'Current buffer outline:') table.insert(lines, '`' .. buf.filename .. '`') From 4089777190c689e6f6464e2f5596fc7667a9914f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 09:22:01 +0100 Subject: [PATCH 302/338] style: improve debug output readability Add empty lines between sections in debug info to make the output more readable and easier to scan visually. --- lua/CopilotChat/ui/debug.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/CopilotChat/ui/debug.lua b/lua/CopilotChat/ui/debug.lua index 4ab9ae81..be4b3b46 100644 --- a/lua/CopilotChat/ui/debug.lua +++ b/lua/CopilotChat/ui/debug.lua @@ -43,7 +43,9 @@ local function build_debug_info() ) ) end + table.insert(lines, '') end + table.insert(lines, 'Current buffer outline:') table.insert(lines, '`' .. buf.filename .. '`') table.insert(lines, '```' .. buf.filetype) From eb5713165ffd0aeec54e97a7df74652edaa7a2b1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 00:32:53 +0100 Subject: [PATCH 303/338] feat: add URL context support using lynx Adds new URL context that allows including content of URLs in chat context using lynx text browser. This enables users to reference external content in their chat prompts. Also improves documentation around optional dependencies and fixes curl utility functions to work with optional parameters. Signed-off-by: Tomas Slusny --- README.md | 17 +++++++++++++---- lua/CopilotChat/config.lua | 14 ++++++++++++++ lua/CopilotChat/context.lua | 22 ++++++++++++++++++++++ lua/CopilotChat/health.lua | 9 +++++++++ lua/CopilotChat/utils.lua | 8 ++++---- 5 files changed, 62 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8b0e2092..99b7047c 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,15 @@ Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabl Optional: -- tiktoken_core: `sudo luarocks install --lua-version 5.1 tiktoken_core`. Alternatively, download a pre-built binary from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases) -- You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. - -> For Arch Linux user, you can install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from aur! +- [tiktoken_core](https://github.com/gptlang/lua-tiktoken) + - Used for more accurate token counting + - Install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` + - Alternatively, download a pre-built binary from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases). You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. + - For Arch Linux users, you can install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from aur +- [lynx](https://lynx.invisible-island.net/) + - Used for fetching textual representation of URLs for `url` context + - For Arch Linux users, you can install [`lynx`](https://archlinux.org/packages/extra/x86_64/lynx) from the official repositories + - For other systems, use your package manager to install `lynx`. For windows use the installer provided from lynx site > [!WARNING] > If you are on neovim < 0.11.0, you also might want to add `noinsert` and `popup` to your `completeopt` to make the chat completion behave well. @@ -277,6 +282,7 @@ Default contexts are: - `files` - Includes all non-hidden files in the current workspace in chat context. By default includes only filenames, includes also content with `full` input. Including all content can be slow on big workspaces so use with care. Supports input. - `git` - Includes current git diff in chat context (default unstaged). Supports input. - `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. +- `url` - Includes content of provided URL in chat context. Requires `lynx` (see requirements). Supports input. You can define custom contexts like this: @@ -495,6 +501,9 @@ Also see [here](/lua/CopilotChat/config.lua): register = { -- see config.lua for implementation }, + url = { + -- see config.lua for implementation + }, }, -- default prompts diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index f413b906..be4fc2ff 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -273,6 +273,20 @@ return { } end, }, + url = { + description = 'Includes content of provided URL in chat context. Requires `lynx`. Supports input.', + input = function(callback) + vim.ui.input({ + prompt = 'Enter URL> ', + default = 'https://', + }, callback) + end, + resolve = function(input) + return { + context.url(input), + } + end, + }, }, -- default prompts diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index d55449cc..ba8e7456 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -19,6 +19,7 @@ local async = require('plenary.async') local log = require('plenary.log') local utils = require('CopilotChat.utils') local file_cache = {} +local url_cache = {} local M = {} @@ -400,6 +401,27 @@ function M.buffer(bufnr) ) end +--- Get the content of an URL +---@param url string +---@return CopilotChat.context.embed? +function M.url(url) + local content = url_cache[url] + if not content then + local out = utils.system({ 'lynx', '-dump', url }) + if not out or out.code ~= 0 then + return nil + end + content = out.stdout + url_cache[url] = content + end + + return { + content = content, + filename = url, + filetype = 'text', + } +end + --- Get current git diff ---@param type string? ---@param winnr number diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index b6f7ebb0..d8ffed6c 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -79,6 +79,15 @@ function M.check() ok('git: ' .. git_version) end + local lynx_version = run_command('lynx', '-version') + if lynx_version == false then + warn( + 'lynx: missing, optional for fetching url contents. See "https://lynx.invisible-island.net/".' + ) + else + ok('lynx: ' .. lynx_version) + end + start('CopilotChat.nvim [dependencies]') if lualib_installed('plenary') then diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index caa42579..adfe1ff4 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -267,11 +267,11 @@ end --- Send curl get request ---@param url string The url ----@param opts table The options +---@param opts table? The options M.curl_get = async.wrap(function(url, opts, callback) curl.get( url, - vim.tbl_deep_extend('force', opts, { + vim.tbl_deep_extend('force', opts or {}, { callback = callback, on_error = function(err) err = M.make_string(err and err.stderr or err) @@ -283,11 +283,11 @@ end, 3) --- Send curl post request ---@param url string The url ----@param opts table The options +---@param opts table? The options M.curl_post = async.wrap(function(url, opts, callback) curl.post( url, - vim.tbl_deep_extend('force', opts, { + vim.tbl_deep_extend('force', opts or {}, { callback = callback, on_error = function(err) err = M.make_string(err and err.stderr or err) From 121475df220d0cc3109c5b949753bf33bd38d3be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 Nov 2024 09:50:27 +0000 Subject: [PATCH 304/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 126135b6..9021255f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -62,15 +62,17 @@ enabled. Optional: -- tiktoken_core: `sudo luarocks install --lua-version 5.1 tiktoken_core`. Alternatively, download a pre-built binary from lua-tiktoken releases -- You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. +- tiktoken_core + - Used for more accurate token counting + - Install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` + - Alternatively, download a pre-built binary from lua-tiktoken releases . You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. + - For Arch Linux users, you can install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from aur +- lynx + - Used for fetching textual representation of URLs for `url` context + - For Arch Linux users, you can install `lynx` from the official repositories + - For other systems, use your package manager to install `lynx`. For windows use the installer provided from lynx site - For Arch Linux user, you can install `luajit-tiktoken-bin` - or - `lua51-tiktoken-bin` - from aur! - [!WARNING] If you are on neovim < 0.11.0, you also might want to add `noinsert` and `popup` to your `completeopt` to make the chat completion behave well. @@ -323,6 +325,7 @@ prompt by using `:` followed by the input (or pressing `complete` key after - `files` - Includes all non-hidden files in the current workspace in chat context. By default includes only filenames, includes also content with `full` input. Including all content can be slow on big workspaces so use with care. Supports input. - `git` - Includes current git diff in chat context (default unstaged). Supports input. - `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. +- `url` - Includes content of provided URL in chat context. Requires `lynx` (see requirements). Supports input. You can define custom contexts like this: @@ -546,6 +549,9 @@ Also see here : register = { -- see config.lua for implementation }, + url = { + -- see config.lua for implementation + }, }, -- default prompts From 40fbf7a5ae0900e03896231237b1b4414c887e6e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 17:56:34 +0100 Subject: [PATCH 305/338] docs: improve documentation clarity and organization * Restructure prerequisites section into Required and Optional * Clarify context documentation with more detailed descriptions * Add missing git/lynx dependency information * Enhance readability of context input descriptions * Fix incorrect function parameter documentation * Make context parameter handling more consistent Signed-off-by: Tomas Slusny --- README.md | 41 +++++++++++------- lua/CopilotChat/config.lua | 83 +++++++++++++++++++----------------- lua/CopilotChat/context.lua | 21 +++++---- lua/CopilotChat/ui/debug.lua | 2 +- lua/CopilotChat/utils.lua | 15 +++++++ 5 files changed, 99 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 99b7047c..034919de 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ - [Lazy.nvim](#lazy.nvim) - [Vim-Plug](#vim-plug) - [Manual](#manual) - - [Post-Installation](#post-installation) - [Usage](#usage) - [Commands](#commands) - [Chat Mappings](#chat-mappings) @@ -38,19 +37,25 @@ ## Prerequisites -Ensure you have the following installed: +- [Neovim 0.9.5+](https://neovim.io/) + - Older versions are not supported, and for best compatibility 0.10.0+ is preferred +- [curl](https://curl.se/) + - 8.0.0+ is recommended for best compatibility + - Should be installed by default on most systems and also shipped with Neovim -- **Neovim stable (0.9.5) or nightly**. +Also make sure [Copilot chat in the IDE](https://github.com/settings/copilot) setting is enabled in GitHub settings. -Verify "[Copilot chat in the IDE](https://github.com/settings/copilot)" is enabled. - -Optional: +**Optional:** - [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - Used for more accurate token counting - - Install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - - Alternatively, download a pre-built binary from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases). You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. - For Arch Linux users, you can install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from aur + - Alternatively, install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` + - Alternatively, download a pre-built binary from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases). You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. +- [git](https://git-scm.com/) + - Used for fetching git diffs for `git` context + - For Arch Linux users, you can install [`git`](https://archlinux.org/packages/extra/x86_64/git) from the official repositories + - For other systems, use your package manager to install `git`. For windows use the installer provided from git site - [lynx](https://lynx.invisible-island.net/) - Used for fetching textual representation of URLs for `url` context - For Arch Linux users, you can install [`lynx`](https://archlinux.org/packages/extra/x86_64/lynx) from the official repositories @@ -276,13 +281,17 @@ Any amount of context can be added to the prompt. If context supports input, you can set the input in the prompt by using `:` followed by the input (or pressing `complete` key after `:`). Default contexts are: -- `buffer` - Includes specified buffer in chat context (default current). Supports input. -- `buffers` - Includes all buffers in chat context (default listed). Supports input. +- `buffer` - Includes specified buffer in chat context. Supports input (default current). +- `buffers` - Includes all buffers in chat context. Supports input (default listed). - `file` - Includes content of provided file in chat context. Supports input. -- `files` - Includes all non-hidden files in the current workspace in chat context. By default includes only filenames, includes also content with `full` input. Including all content can be slow on big workspaces so use with care. Supports input. -- `git` - Includes current git diff in chat context (default unstaged). Supports input. -- `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. -- `url` - Includes content of provided URL in chat context. Requires `lynx` (see requirements). Supports input. +- `files` - Includes all non-hidden files in the current workspace in chat context. Supports input (default list). + - `files:list` - Only lists file names. + - `files:content` - Includes file content for each file found. Can be slow on large workspaces, use with care. +- `git` - Requires `git`. Includes current git diff in chat context. Supports input (default unstaged). + - `git:unstaged` - Includes unstaged changes in chat context. + - `git:staged` - Includes staged changes in chat context. +- `url` - Requires `lynx`. Includes content of provided URL in chat context. Supports input. +- `register` - Includes contents of register in chat context. Supports input (default +, e.g clipboard). You can define custom contexts like this: @@ -498,10 +507,10 @@ Also see [here](/lua/CopilotChat/config.lua): git = { -- see config.lua for implementation }, - register = { + url = { -- see config.lua for implementation }, - url = { + register = { -- see config.lua for implementation }, }, diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index be4fc2ff..4c760d47 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -146,7 +146,7 @@ return { -- default contexts contexts = { buffer = { - description = 'Includes specified buffer in chat context (default current). Supports input.', + description = 'Includes specified buffer in chat context. Supports input (default current).', input = function(callback) vim.ui.select( vim.tbl_map( @@ -169,13 +169,14 @@ return { ) end, resolve = function(input, source) + input = input and tonumber(input) or source.bufnr return { - context.buffer(input and tonumber(input) or source.bufnr), + context.buffer(input), } end, }, buffers = { - description = 'Includes all buffers in chat context (default listed). Supports input.', + description = 'Includes all buffers in chat context. Supports input (default listed).', input = function(callback) vim.ui.select({ 'listed', 'visible' }, { prompt = 'Select buffer scope> ', @@ -212,33 +213,58 @@ return { end, }, files = { - description = 'Includes all non-hidden files in the current workspace in chat context. By default includes only filenames, includes also content with `full` input. Including all content can be slow on big workspaces so use with care. Supports input.', + description = 'Includes all non-hidden files in the current workspace in chat context. Supports input (default list).', input = function(callback) - vim.ui.select({ 'list', 'full' }, { + local choices = utils.kv_list({ + list = 'Only lists file names', + full = 'Includes file content for each file found. Can be slow on large workspaces, use with care.', + }) + + vim.ui.select(choices, { prompt = 'Select files content> ', - }, callback) + format_item = function(choice) + return choice.key .. ' - ' .. choice.value + end, + }, function(choice) + callback(choice and choice.key) + end) end, resolve = function(input, source) return context.files(source.winnr, input == 'full') end, }, git = { - description = 'Includes current git diff in chat context (default unstaged). Supports input.', + description = 'Requires `git`. Includes current git diff in chat context. Supports input (default unstaged).', input = function(callback) vim.ui.select({ 'unstaged', 'staged' }, { prompt = 'Select diff type> ', }, callback) end, resolve = function(input, source) + input = input or 'unstaged' return { context.gitdiff(input, source.winnr), } end, }, + url = { + description = 'Requires `lynx`. Includes content of provided URL in chat context. Supports input.', + input = function(callback) + vim.ui.input({ + prompt = 'Enter URL> ', + default = 'https://', + }, callback) + end, + resolve = function(input) + return { + context.url(input), + } + end, + }, register = { - description = 'Includes contents of register in chat context (default +, e.g clipboard). Supports input.', + description = 'Includes contents of register in chat context. Supports input (default +, e.g clipboard).', input = function(callback) - local registers = { + local choices = utils.kv_list({ ['+'] = 'synchronized with the system clipboard', ['*'] = 'synchronized with the selection clipboard', ['"'] = 'last deleted, changed, or yanked content', @@ -250,43 +276,24 @@ return { ['#'] = 'alternate buffer', ['='] = 'result of an expression', ['/'] = 'last search pattern', - } + }) - vim.ui.select( - vim.tbl_map(function(k) - return { id = k, name = k .. ' - ' .. (registers[k] or '') } - end, vim.tbl_keys(registers)), - { - prompt = 'Select a register> ', - format_item = function(item) - return item.name - end, - }, - function(choice) - callback(choice and choice.id) - end - ) + vim.ui.select(choices, { + prompt = 'Select a register> ', + format_item = function(choice) + return choice.key .. ' - ' .. choice.value + end, + }, function(choice) + callback(choice and choice.key) + end) end, resolve = function(input) + input = input or '+' return { context.register(input), } end, }, - url = { - description = 'Includes content of provided URL in chat context. Requires `lynx`. Supports input.', - input = function(callback) - vim.ui.input({ - prompt = 'Enter URL> ', - default = 'https://', - }, callback) - end, - resolve = function(input) - return { - context.url(input), - } - end, - }, }, -- default prompts diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index ba8e7456..eb4a19ad 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -307,7 +307,7 @@ end --- Get list of all files in workspace ---@param winnr number? ----@param with_content boolean? +---@param with_content boolean ---@return table function M.files(winnr, with_content) local cwd = utils.win_cwd(winnr) @@ -366,9 +366,13 @@ function M.files(winnr, with_content) end --- Get the content of a file ----@param filename string +---@param filename? string ---@return CopilotChat.context.embed? function M.file(filename) + if not filename or filename == '' then + return nil + end + async.util.scheduler() local ft = utils.filetype(filename) if not ft then @@ -379,11 +383,10 @@ function M.file(filename) end --- Get the content of a buffer ----@param bufnr? number +---@param bufnr number ---@return CopilotChat.context.embed? function M.buffer(bufnr) async.util.scheduler() - bufnr = bufnr or vim.api.nvim_get_current_buf() if not utils.buf_valid(bufnr) then return nil @@ -405,6 +408,10 @@ end ---@param url string ---@return CopilotChat.context.embed? function M.url(url) + if not url or url == '' then + return nil + end + local content = url_cache[url] if not content then local out = utils.system({ 'lynx', '-dump', url }) @@ -423,11 +430,10 @@ function M.url(url) end --- Get current git diff ----@param type string? +---@param type string ---@param winnr number ---@return CopilotChat.context.embed? function M.gitdiff(type, winnr) - type = type or 'unstaged' local cwd = utils.win_cwd(winnr) local cmd = { 'git', @@ -452,10 +458,9 @@ function M.gitdiff(type, winnr) end --- Return contents of specified register ----@param register string? +---@param register string ---@return CopilotChat.context.embed? function M.register(register) - register = register or '+' local lines = vim.fn.getreg(register) if not lines or lines == '' then return nil diff --git a/lua/CopilotChat/ui/debug.lua b/lua/CopilotChat/ui/debug.lua index be4b3b46..16f9893c 100644 --- a/lua/CopilotChat/ui/debug.lua +++ b/lua/CopilotChat/ui/debug.lua @@ -24,7 +24,7 @@ local function build_debug_info() '', } - local buf = context.buffer() + local buf = context.buffer(0) if buf then if buf.symbols then table.insert(lines, 'Current buffer symbols:') diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index adfe1ff4..83745249 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -162,6 +162,21 @@ function M.debounce(id, fn, delay) M.timers[id] = vim.defer_fn(fn, delay) end +--- Create key-value list from table +---@param tbl table The table +---@return table +function M.kv_list(tbl) + local result = {} + for k, v in pairs(tbl) do + table.insert(result, { + key = k, + value = v, + }) + end + + return result +end + --- Check if a buffer is valid ---@param bufnr number? The buffer number ---@return boolean From 1a1eef96ac48ccd0249c35adfaa22473274460d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 Nov 2024 17:00:05 +0000 Subject: [PATCH 306/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9021255f..2d62d0ee 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -30,7 +30,6 @@ Table of Contents *CopilotChat-table-of-contents* - |CopilotChat-lazy.nvim| - |CopilotChat-vim-plug| - |CopilotChat-manual| - - |CopilotChat-post-installation| - |CopilotChat-usage| - |CopilotChat-commands| - |CopilotChat-chat-mappings| @@ -53,20 +52,26 @@ Table of Contents *CopilotChat-table-of-contents* PREREQUISITES *CopilotChat-prerequisites* -Ensure you have the following installed: +- Neovim 0.9.5+ + - Older versions are not supported, and for best compatibility 0.10.0+ is preferred +- curl + - 8.0.0+ is recommended for best compatibility + - Should be installed by default on most systems and also shipped with Neovim -- **Neovim stable (0.9.5) or nightly**. +Also make sure Copilot chat in the IDE +setting is enabled in GitHub settings. -Verify "Copilot chat in the IDE " is -enabled. - -Optional: +**Optional:** - tiktoken_core - Used for more accurate token counting - - Install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - - Alternatively, download a pre-built binary from lua-tiktoken releases . You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. - For Arch Linux users, you can install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from aur + - Alternatively, install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` + - Alternatively, download a pre-built binary from lua-tiktoken releases . You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. +- git + - Used for fetching git diffs for `git` context + - For Arch Linux users, you can install `git` from the official repositories + - For other systems, use your package manager to install `git`. For windows use the installer provided from git site - lynx - Used for fetching textual representation of URLs for `url` context - For Arch Linux users, you can install `lynx` from the official repositories @@ -319,13 +324,17 @@ be added to the prompt. If context supports input, you can set the input in the prompt by using `:` followed by the input (or pressing `complete` key after `:`). Default contexts are: -- `buffer` - Includes specified buffer in chat context (default current). Supports input. -- `buffers` - Includes all buffers in chat context (default listed). Supports input. +- `buffer` - Includes specified buffer in chat context. Supports input (default current). +- `buffers` - Includes all buffers in chat context. Supports input (default listed). - `file` - Includes content of provided file in chat context. Supports input. -- `files` - Includes all non-hidden files in the current workspace in chat context. By default includes only filenames, includes also content with `full` input. Including all content can be slow on big workspaces so use with care. Supports input. -- `git` - Includes current git diff in chat context (default unstaged). Supports input. -- `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input. -- `url` - Includes content of provided URL in chat context. Requires `lynx` (see requirements). Supports input. +- `files` - Includes all non-hidden files in the current workspace in chat context. Supports input (default list). + - `files:list` - Only lists file names. + - `files:content` - Includes file content for each file found. Can be slow on large workspaces, use with care. +- `git` - Requires `git`. Includes current git diff in chat context. Supports input (default unstaged). + - `git:unstaged` - Includes unstaged changes in chat context. + - `git:staged` - Includes staged changes in chat context. +- `url` - Requires `lynx`. Includes content of provided URL in chat context. Supports input. +- `register` - Includes contents of register in chat context. Supports input (default +, e.g clipboard). You can define custom contexts like this: @@ -546,10 +555,10 @@ Also see here : git = { -- see config.lua for implementation }, - register = { + url = { -- see config.lua for implementation }, - url = { + register = { -- see config.lua for implementation }, }, From 640ddbc42ea970883bd623e3ed818bf2f9d38cd2 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 18:08:13 +0100 Subject: [PATCH 307/338] docs(readme): reorganize requirements section Improve readability of requirements section by: - Renaming "Prerequisites" to "Requirements" - Consolidating required and optional dependencies into a cleaner list format - Making the documentation more concise while preserving all information - Adding links to package manager sections Signed-off-by: Tomas Slusny --- README.md | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 034919de..6bcd8207 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ > [!NOTE] > Plugin was rewritten to Lua from Python. Please check the [migration guide from version 1 to version 2](/MIGRATION.md) for more information. -- [Prerequisites](#prerequisites) +- [Requirements](#requirements) - [Installation](#installation) - - [Lazy.nvim](#lazy.nvim) + - [Lazy.nvim](#lazynvim) - [Vim-Plug](#vim-plug) - [Manual](#manual) - [Usage](#usage) @@ -35,29 +35,19 @@ - [Development](#development) - [Contributors ✨](#contributors-) -## Prerequisites +## Requirements -- [Neovim 0.9.5+](https://neovim.io/) - - Older versions are not supported, and for best compatibility 0.10.0+ is preferred -- [curl](https://curl.se/) - - 8.0.0+ is recommended for best compatibility - - Should be installed by default on most systems and also shipped with Neovim - -Also make sure [Copilot chat in the IDE](https://github.com/settings/copilot) setting is enabled in GitHub settings. - -**Optional:** - -- [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - - Used for more accurate token counting +- [Neovim 0.9.5+](https://neovim.io/) - Older versions are not supported, and for best compatibility 0.10.0+ is preferred +- [curl](https://curl.se/) - 8.0.0+ is recommended for best compatibility. Should be installed by default on most systems and also shipped with Neovim +- [Copilot chat in the IDE](https://github.com/settings/copilot) setting enabled in GitHub settings. +- _Optional:_ [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - Used for more accurate token counting - For Arch Linux users, you can install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from aur - Alternatively, install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Alternatively, download a pre-built binary from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases). You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. -- [git](https://git-scm.com/) - - Used for fetching git diffs for `git` context +- _Optional:_ [git](https://git-scm.com/) - Used for fetching git diffs for `git` context - For Arch Linux users, you can install [`git`](https://archlinux.org/packages/extra/x86_64/git) from the official repositories - For other systems, use your package manager to install `git`. For windows use the installer provided from git site -- [lynx](https://lynx.invisible-island.net/) - - Used for fetching textual representation of URLs for `url` context +- _Optional:_ [lynx](https://lynx.invisible-island.net/) - Used for fetching textual representation of URLs for `url` context - For Arch Linux users, you can install [`lynx`](https://archlinux.org/packages/extra/x86_64/lynx) from the official repositories - For other systems, use your package manager to install `lynx`. For windows use the installer provided from lynx site @@ -66,7 +56,7 @@ Also make sure [Copilot chat in the IDE](https://github.com/settings/copilot) se ## Installation -### Lazy.nvim +### [Lazy.nvim](https://github.com/folke/lazy.nvim) ```lua return { @@ -88,7 +78,7 @@ return { See @jellydn for [configuration](https://github.com/jellydn/lazy-nvim-ide/blob/main/lua/plugins/extras/copilot-chat-v2.lua) -### Vim-Plug +### [Vim-Plug](https://github.com/junegunn/vim-plug) Similar to the lazy setup, you can use the following configuration: From 60cc97bab2fe9862ae5450a9ffd3f722d671d365 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 Nov 2024 17:10:40 +0000 Subject: [PATCH 308/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 2d62d0ee..ef9eb209 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -4,7 +4,7 @@ Table of Contents *CopilotChat-table-of-contents* 1. Copilot Chat for Neovim |CopilotChat-copilot-chat-for-neovim| - - Prerequisites |CopilotChat-prerequisites| + - Requirements |CopilotChat-requirements| - Installation |CopilotChat-installation| - Usage |CopilotChat-usage| - Configuration |CopilotChat-configuration| @@ -25,7 +25,7 @@ Table of Contents *CopilotChat-table-of-contents* [!NOTE] Plugin was rewritten to Lua from Python. Please check the migration guide from version 1 to version 2 for more information. -- |CopilotChat-prerequisites| +- |CopilotChat-requirements| - |CopilotChat-installation| - |CopilotChat-lazy.nvim| - |CopilotChat-vim-plug| @@ -50,30 +50,19 @@ Table of Contents *CopilotChat-table-of-contents* - |CopilotChat-contributors-✨| -PREREQUISITES *CopilotChat-prerequisites* +REQUIREMENTS *CopilotChat-requirements* -- Neovim 0.9.5+ - - Older versions are not supported, and for best compatibility 0.10.0+ is preferred -- curl - - 8.0.0+ is recommended for best compatibility - - Should be installed by default on most systems and also shipped with Neovim - -Also make sure Copilot chat in the IDE -setting is enabled in GitHub settings. - -**Optional:** - -- tiktoken_core - - Used for more accurate token counting +- Neovim 0.9.5+ - Older versions are not supported, and for best compatibility 0.10.0+ is preferred +- curl - 8.0.0+ is recommended for best compatibility. Should be installed by default on most systems and also shipped with Neovim +- Copilot chat in the IDE setting enabled in GitHub settings. +- _Optional:_ tiktoken_core - Used for more accurate token counting - For Arch Linux users, you can install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from aur - Alternatively, install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Alternatively, download a pre-built binary from lua-tiktoken releases . You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. -- git - - Used for fetching git diffs for `git` context +- _Optional:_ git - Used for fetching git diffs for `git` context - For Arch Linux users, you can install `git` from the official repositories - For other systems, use your package manager to install `git`. For windows use the installer provided from git site -- lynx - - Used for fetching textual representation of URLs for `url` context +- _Optional:_ lynx - Used for fetching textual representation of URLs for `url` context - For Arch Linux users, you can install `lynx` from the official repositories - For other systems, use your package manager to install `lynx`. For windows use the installer provided from lynx site From 9a810e4e56eb31aef9e3175518325f0bc4b7aabc Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 18:25:22 +0100 Subject: [PATCH 309/338] docs: improve README formatting and add prompt command info Improve README formatting by making the optional dependencies more consistent and add information about the :CopilotChat command to the list of available commands. - Make (Optional) prefix consistent across all dependencies - Remove trailing period from Copilot chat setting requirement - Add documentation for :CopilotChat command usage --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6bcd8207..9caec9e2 100644 --- a/README.md +++ b/README.md @@ -39,15 +39,15 @@ - [Neovim 0.9.5+](https://neovim.io/) - Older versions are not supported, and for best compatibility 0.10.0+ is preferred - [curl](https://curl.se/) - 8.0.0+ is recommended for best compatibility. Should be installed by default on most systems and also shipped with Neovim -- [Copilot chat in the IDE](https://github.com/settings/copilot) setting enabled in GitHub settings. -- _Optional:_ [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - Used for more accurate token counting +- [Copilot chat in the IDE](https://github.com/settings/copilot) setting enabled in GitHub settings +- _(Optional)_ [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - Used for more accurate token counting - For Arch Linux users, you can install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from aur - Alternatively, install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Alternatively, download a pre-built binary from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases). You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. -- _Optional:_ [git](https://git-scm.com/) - Used for fetching git diffs for `git` context +- _(Optional)_ [git](https://git-scm.com/) - Used for fetching git diffs for `git` context - For Arch Linux users, you can install [`git`](https://archlinux.org/packages/extra/x86_64/git) from the official repositories - For other systems, use your package manager to install `git`. For windows use the installer provided from git site -- _Optional:_ [lynx](https://lynx.invisible-island.net/) - Used for fetching textual representation of URLs for `url` context +- _(Optional)_ [lynx](https://lynx.invisible-island.net/) - Used for fetching textual representation of URLs for `url` context - For Arch Linux users, you can install [`lynx`](https://archlinux.org/packages/extra/x86_64/lynx) from the official repositories - For other systems, use your package manager to install `lynx`. For windows use the installer provided from lynx site @@ -135,6 +135,7 @@ See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/ma - `:CopilotChatDebugInfo` - Show debug information - `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. - `:CopilotChatAgents` - View and select available agents. This is reset when a new instance is made. Please set your agent in `init.lua` for persistence. +- `:CopilotChat` - Ask a question with a specific prompt. For example, `:CopilotChatExplain` will ask a question with the `Explain` prompt. See [Prompts](#prompts) for more information. ### Chat Mappings From 1bde197859bbbdcac8718157f2f2ffcd8e5fd37d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 Nov 2024 17:27:36 +0000 Subject: [PATCH 310/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ef9eb209..86dcffea 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -54,15 +54,15 @@ REQUIREMENTS *CopilotChat-requirements* - Neovim 0.9.5+ - Older versions are not supported, and for best compatibility 0.10.0+ is preferred - curl - 8.0.0+ is recommended for best compatibility. Should be installed by default on most systems and also shipped with Neovim -- Copilot chat in the IDE setting enabled in GitHub settings. -- _Optional:_ tiktoken_core - Used for more accurate token counting +- Copilot chat in the IDE setting enabled in GitHub settings +- _(Optional)_ tiktoken_core - Used for more accurate token counting - For Arch Linux users, you can install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from aur - Alternatively, install via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Alternatively, download a pre-built binary from lua-tiktoken releases . You can check your Lua PATH in Neovim by doing `:lua print(package.cpath)`. Save the binary as `tiktoken_core.so` in any of the given paths. -- _Optional:_ git - Used for fetching git diffs for `git` context +- _(Optional)_ git - Used for fetching git diffs for `git` context - For Arch Linux users, you can install `git` from the official repositories - For other systems, use your package manager to install `git`. For windows use the installer provided from git site -- _Optional:_ lynx - Used for fetching textual representation of URLs for `url` context +- _(Optional)_ lynx - Used for fetching textual representation of URLs for `url` context - For Arch Linux users, you can install `lynx` from the official repositories - For other systems, use your package manager to install `lynx`. For windows use the installer provided from lynx site @@ -158,6 +158,7 @@ COMMANDS ~ - `:CopilotChatDebugInfo` - Show debug information - `:CopilotChatModels` - View and select available models. This is reset when a new instance is made. Please set your model in `init.lua` for persistence. - `:CopilotChatAgents` - View and select available agents. This is reset when a new instance is made. Please set your agent in `init.lua` for persistence. +- `:CopilotChat` - Ask a question with a specific prompt. For example, `:CopilotChatExplain` will ask a question with the `Explain` prompt. See |CopilotChat-prompts| for more information. CHAT MAPPINGS ~ From 98a1391a3030a9372da4a30da41dd02a7f1b8743 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 20:45:42 +0100 Subject: [PATCH 311/338] refactor: switch to character-based truncation Previously, the content truncation was based on line count which could lead to inconsistent token usage when lines had varying lengths. This change switches to character-based truncation to provide more predictable token consumption. Key changes: - Introduced LINE_CHARACTERS constant (100 chars per line) - Updated BIG_FILE_THRESHOLD and BIG_EMBED_THRESHOLD to use char count - Refactored generate_line_numbers into generate_content_block that handles both line numbering and truncation - Made truncation logic count total characters instead of lines --- lua/CopilotChat/copilot.lua | 67 ++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 6deb0c3b..3a24560b 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -17,8 +17,9 @@ local temp_file = utils.temp_file --- Constants local CONTEXT_FORMAT = '[#file:%s](#file:%s-context)' -local BIG_FILE_THRESHOLD = 2000 -local BIG_EMBED_THRESHOLD = 300 +local LINE_CHARACTERS = 100 +local BIG_FILE_THRESHOLD = 2000 * LINE_CHARACTERS +local BIG_EMBED_THRESHOLD = 200 * LINE_CHARACTERS local EMBED_MODEL = 'text-embedding-3-small' local TRUNCATED = '... (truncated)' local TIMEOUT = 30000 @@ -74,26 +75,35 @@ local function get_cached_token() return nil end ---- Generate line numbers for the given content ----@param content string: The content to generate line numbers for +--- Generate content block with line numbers, truncating if necessary +---@param content string: The content +---@param threshold number: The threshold for truncation ---@param start_line number|nil: The starting line number ---@return string -local function generate_line_numbers(content, start_line) +local function generate_content_block(content, threshold, start_line) local lines = vim.split(content, '\n') - if #lines > BIG_FILE_THRESHOLD then - lines = vim.list_slice(lines, 1, BIG_FILE_THRESHOLD) - table.insert(lines, TRUNCATED) - end + local total_chars = 0 - local total_lines = #lines - local max_length = #tostring(total_lines) for i, line in ipairs(lines) do - local formatted_line_number = string.format('%' .. max_length .. 'd', i - 1 + (start_line or 1)) - lines[i] = formatted_line_number .. ': ' .. line + total_chars = total_chars + #line + if total_chars > threshold then + lines = vim.list_slice(lines, 1, i) + table.insert(lines, TRUNCATED) + break + end end - content = table.concat(lines, '\n') - return content + if start_line ~= -1 then + local total_lines = #lines + local max_length = #tostring(total_lines) + for i, line in ipairs(lines) do + local formatted_line_number = + string.format('%' .. max_length .. 'd', i - 1 + (start_line or 1)) + lines[i] = formatted_line_number .. ': ' .. line + end + end + + return table.concat(lines, '\n') end --- Generate messages for the given selection @@ -122,7 +132,7 @@ local function generate_selection_messages(selection) .. string.format( '```%s\n%s\n```', filetype, - generate_line_numbers(content, selection.start_line) + generate_content_block(content, BIG_FILE_THRESHOLD, selection.start_line) ) if selection.diagnostics then @@ -178,12 +188,15 @@ local function generate_embeddings_messages(embeddings) '# FILE:%s CONTEXT\n```%s\n%s\n```', filename:upper(), filetype, - generate_line_numbers(table.concat( - vim.tbl_map(function(e) - return vim.trim(e.content) - end, group), - '\n' - )) + generate_content_block( + table.concat( + vim.tbl_map(function(e) + return vim.trim(e.content) + end, group), + '\n' + ), + BIG_FILE_THRESHOLD + ) ), role = 'user', }) @@ -261,13 +274,7 @@ local function generate_embedding_request(inputs, model, threshold) return { dimensions = 512, input = vim.tbl_map(function(embedding) - local lines = vim.split(embedding.content, '\n') - if #lines > threshold then - lines = vim.list_slice(lines, 1, threshold) - table.insert(lines, TRUNCATED) - end - local content = table.concat(lines, '\n') - + local content = generate_content_block(embedding.content, threshold, -1) if embedding.filetype == 'raw' then return content else @@ -909,7 +916,7 @@ function Copilot:embed(inputs) attempts = attempts + 1 -- If we have few items and the request failed, try reducing threshold first if #batch <= 5 then - threshold = math.max(5, math.floor(threshold / 2)) + threshold = math.max(5 * LINE_CHARACTERS, math.floor(threshold / 2)) log.debug(string.format('Reducing threshold to %d and retrying...', threshold)) else -- Otherwise reduce batch size first From f0d8a7136a72c13a823d45572ea5b4b8431d5f55 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 21:14:39 +0100 Subject: [PATCH 312/338] feat: add status notifications for background tasks Add a new notification system to provide user feedback for background operations like file scanning, model fetching, and API calls. This improves the user experience by making it clear when the plugin is performing work. The changes include: - New notify module for pub/sub event system - Integration with spinner UI to show status messages - Status notifications for file operations, API calls, and embeddings - Removed redundant logging in favor of user-visible notifications Signed-off-by: Tomas Slusny --- lua/CopilotChat/context.lua | 11 +++++++++++ lua/CopilotChat/copilot.lua | 14 +++++++++++++- lua/CopilotChat/notify.lua | 31 +++++++++++++++++++++++++++++++ lua/CopilotChat/tiktoken.lua | 5 +++-- lua/CopilotChat/ui/spinner.lua | 18 +++++++++++++++--- 5 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 lua/CopilotChat/notify.lua diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index eb4a19ad..026a806c 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -17,6 +17,7 @@ local async = require('plenary.async') local log = require('plenary.log') +local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local file_cache = {} local url_cache = {} @@ -311,11 +312,16 @@ end ---@return table function M.files(winnr, with_content) local cwd = utils.win_cwd(winnr) + + notify.publish(notify.STATUS, 'Scanning files') + local files = utils.scan_dir(cwd, { add_dirs = false, respect_gitignore = true, }) + notify.publish(notify.STATUS, 'Reading files') + local out = {} -- Read all files if we want content as well @@ -373,6 +379,8 @@ function M.file(filename) return nil end + notify.publish(notify.STATUS, 'Reading file ' .. filename) + async.util.scheduler() local ft = utils.filetype(filename) if not ft then @@ -414,6 +422,7 @@ function M.url(url) local content = url_cache[url] if not content then + notify.publish(notify.STATUS, 'Fetching ' .. url) local out = utils.system({ 'lynx', '-dump', url }) if not out or out.code ~= 0 then return nil @@ -434,6 +443,8 @@ end ---@param winnr number ---@return CopilotChat.context.embed? function M.gitdiff(type, winnr) + notify.publish(notify.STATUS, 'Fetching git diff') + local cwd = utils.win_cwd(winnr) local cmd = { 'git', diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 3a24560b..065a3609 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -11,6 +11,7 @@ local log = require('plenary.log') local prompts = require('CopilotChat.prompts') local tiktoken = require('CopilotChat.tiktoken') +local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local class = utils.class local temp_file = utils.temp_file @@ -405,6 +406,8 @@ function Copilot:fetch_models() return self.models end + notify.publish(notify.STATUS, 'Fetching models') + local response, err = utils.curl_get( 'https://api.githubcopilot.com/models', vim.tbl_extend('force', self.request_args, { @@ -433,7 +436,6 @@ function Copilot:fetch_models() end end - log.info('Models fetched') log.trace(models) self.models = out return out @@ -446,6 +448,8 @@ function Copilot:fetch_agents() return self.agents end + notify.publish(notify.STATUS, 'Fetching agents') + local response, err = utils.curl_get( 'https://api.githubcopilot.com/agents', vim.tbl_extend('force', self.request_args, { @@ -482,6 +486,8 @@ function Copilot:enable_policy(model) return end + notify.publish(notify.STATUS, 'Enabling ' .. model .. ' policy') + local response, err = utils.curl_post( 'https://api.githubcopilot.com/models/' .. model .. '/policy', vim.tbl_extend('force', self.request_args, { @@ -611,6 +617,8 @@ function Copilot:ask(prompt, opts) return end + notify.publish(notify.STATUS, '') + local ok, content = pcall(vim.json.decode, line, { luanil = { object = true, @@ -724,6 +732,8 @@ function Copilot:ask(prompt, opts) args.stream = stream_func end + notify.publish(notify.STATUS, 'Thinking') + local response, err = utils.curl_post(url, args) if self.current_job ~= job_id then @@ -867,6 +877,8 @@ function Copilot:embed(inputs) return {} end + notify.publish(notify.STATUS, 'Generating embeddings for ' .. #inputs .. ' inputs') + -- Initialize essentials local model = EMBED_MODEL local to_process = {} diff --git a/lua/CopilotChat/notify.lua b/lua/CopilotChat/notify.lua new file mode 100644 index 00000000..fd04a97f --- /dev/null +++ b/lua/CopilotChat/notify.lua @@ -0,0 +1,31 @@ +local log = require('plenary.log') + +local M = {} + +M.STATUS = 'status' + +M.listeners = {} + +--- Publish an event with a message +---@param event_name string +---@param data any +function M.publish(event_name, data) + if M.listeners[event_name] then + log.debug(event_name .. ':', data) + for _, callback in ipairs(M.listeners[event_name]) do + callback(data) + end + end +end + +--- Listen for an event +---@param event_name string +---@param callback fun(data:any) +function M.listen(event_name, callback) + if not M.listeners[event_name] then + M.listeners[event_name] = {} + end + table.insert(M.listeners[event_name], callback) +end + +return M diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index e0cdba53..97f1d25d 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -1,5 +1,5 @@ local async = require('plenary.async') -local log = require('plenary.log') +local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local current_tokenizer = nil local cache_dir = vim.fn.stdpath('cache') @@ -22,7 +22,8 @@ local function load_tiktoken_data(tokenizer) return cache_path end - log.info('Downloading tiktoken data from ' .. tiktoken_url) + notify.publish(notify.STATUS, 'Downloading tiktoken data from ' .. tiktoken_url) + utils.curl_get(tiktoken_url, { output = cache_path, }) diff --git a/lua/CopilotChat/ui/spinner.lua b/lua/CopilotChat/ui/spinner.lua index d07e25e3..a55a36ae 100644 --- a/lua/CopilotChat/ui/spinner.lua +++ b/lua/CopilotChat/ui/spinner.lua @@ -1,3 +1,4 @@ +local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local class = utils.class @@ -19,11 +20,17 @@ local spinner_frames = { ---@field bufnr number ---@field timer table ---@field index number +---@field status string? local Spinner = class(function(self, bufnr) self.ns = vim.api.nvim_create_namespace('copilot-chat-spinner') self.bufnr = bufnr self.timer = nil self.index = 1 + self.status = nil + + notify.listen(notify.STATUS, function(status) + self.status = tostring(status) + end) end) function Spinner:start() @@ -41,6 +48,11 @@ function Spinner:start() return end + local frame = spinner_frames[self.index] + if self.status then + frame = self.status .. ' ' .. frame + end + vim.api.nvim_buf_set_extmark( self.bufnr, self.ns, @@ -50,9 +62,9 @@ function Spinner:start() id = 1, hl_mode = 'combine', priority = 100, - virt_text = vim.tbl_map(function(t) - return { t, 'CopilotChatSpinner' } - end, vim.split(spinner_frames[self.index], '\n')), + virt_text = { + { frame, 'CopilotChatSpinner' }, + }, } ) From 839d5a21722e7af139722e8f5d91f9137d77ec77 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 29 Nov 2024 22:55:54 +0100 Subject: [PATCH 313/338] feat: make lynx optional for URL context Previously, lynx was required for the URL context to work. This change makes lynx optional by adding a fallback to curl when lynx is not available or fails. The fallback implementation includes HTML content cleanup to provide readable text. When lynx is available, it will be used for better text formatting, but the URL context will still work without it using the curl fallback. --- README.md | 4 ++-- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/context.lua | 42 +++++++++++++++++++++++++++++++++---- lua/CopilotChat/health.lua | 2 +- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9caec9e2..c83afdc8 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ - _(Optional)_ [git](https://git-scm.com/) - Used for fetching git diffs for `git` context - For Arch Linux users, you can install [`git`](https://archlinux.org/packages/extra/x86_64/git) from the official repositories - For other systems, use your package manager to install `git`. For windows use the installer provided from git site -- _(Optional)_ [lynx](https://lynx.invisible-island.net/) - Used for fetching textual representation of URLs for `url` context +- _(Optional)_ [lynx](https://lynx.invisible-island.net/) - Used for improved fetching of URLs for `url` context - For Arch Linux users, you can install [`lynx`](https://archlinux.org/packages/extra/x86_64/lynx) from the official repositories - For other systems, use your package manager to install `lynx`. For windows use the installer provided from lynx site @@ -281,7 +281,7 @@ Default contexts are: - `git` - Requires `git`. Includes current git diff in chat context. Supports input (default unstaged). - `git:unstaged` - Includes unstaged changes in chat context. - `git:staged` - Includes staged changes in chat context. -- `url` - Requires `lynx`. Includes content of provided URL in chat context. Supports input. +- `url` - Includes content of provided URL in chat context. Supports input. - `register` - Includes contents of register in chat context. Supports input (default +, e.g clipboard). You can define custom contexts like this: diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 4c760d47..d2a1974f 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -248,7 +248,7 @@ return { end, }, url = { - description = 'Requires `lynx`. Includes content of provided URL in chat context. Supports input.', + description = 'Includes content of provided URL in chat context. Supports input.', input = function(callback) vim.ui.input({ prompt = 'Enter URL> ', diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 026a806c..2921ad86 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -423,11 +423,45 @@ function M.url(url) local content = url_cache[url] if not content then notify.publish(notify.STATUS, 'Fetching ' .. url) - local out = utils.system({ 'lynx', '-dump', url }) - if not out or out.code ~= 0 then - return nil + + local ok, out = async.util.apcall(utils.system, { 'lynx', '-dump', url }) + if ok and out and out.code == 0 then + -- Use lynx to fetch content + content = out.stdout + else + -- Fallback to curl if lynx fails + local response = utils.curl_get(url, { raw = { '-L' } }) + if not response or not response.body then + return nil + end + + content = vim.trim(response + .body + -- Remove script, style tags and their contents first + :gsub( + '', + '' + ) + :gsub('', '') + -- Remove XML/CDATA in one go + :gsub('', '') + -- Remove all HTML tags (both opening and closing) in one go + :gsub( + '<%/?%w+[^>]*>', + ' ' + ) + -- Handle common HTML entities + :gsub('&(%w+);', { + nbsp = ' ', + lt = '<', + gt = '>', + amp = '&', + quot = '"', + }) + -- Remove any remaining HTML entities (numeric or named) + :gsub('&#?%w+;', '')) end - content = out.stdout + url_cache[url] = content end diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index d8ffed6c..3908c74c 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -82,7 +82,7 @@ function M.check() local lynx_version = run_command('lynx', '-version') if lynx_version == false then warn( - 'lynx: missing, optional for fetching url contents. See "https://lynx.invisible-island.net/".' + 'lynx: missing, optional for improved fetching of url contents. See "https://lynx.invisible-island.net/".' ) else ok('lynx: ' .. lynx_version) From b03f0614e1eb170b7fd0306b2eabd90b21fe8a74 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 Nov 2024 21:57:59 +0000 Subject: [PATCH 314/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 86dcffea..0dcb07f5 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -62,7 +62,7 @@ REQUIREMENTS *CopilotChat-requirements* - _(Optional)_ git - Used for fetching git diffs for `git` context - For Arch Linux users, you can install `git` from the official repositories - For other systems, use your package manager to install `git`. For windows use the installer provided from git site -- _(Optional)_ lynx - Used for fetching textual representation of URLs for `url` context +- _(Optional)_ lynx - Used for improved fetching of URLs for `url` context - For Arch Linux users, you can install `lynx` from the official repositories - For other systems, use your package manager to install `lynx`. For windows use the installer provided from lynx site @@ -323,7 +323,7 @@ prompt by using `:` followed by the input (or pressing `complete` key after - `git` - Requires `git`. Includes current git diff in chat context. Supports input (default unstaged). - `git:unstaged` - Includes unstaged changes in chat context. - `git:staged` - Includes staged changes in chat context. -- `url` - Requires `lynx`. Includes content of provided URL in chat context. Supports input. +- `url` - Includes content of provided URL in chat context. Supports input. - `register` - Includes contents of register in chat context. Supports input (default +, e.g clipboard). You can define custom contexts like this: From 95409ac21a2e680e89e37ecfd053e4385934fadd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 30 Nov 2024 00:24:01 +0100 Subject: [PATCH 315/338] style: link spinner highlight to DiagnosticInfo Change CopilotChatSpinner highlight group to link to DiagnosticInfo instead of CursorColumn for better visual consistency with other UI elements. --- lua/CopilotChat/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 5c49311b..b7701a57 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -924,7 +924,7 @@ function M.setup(config) M.log_level(M.config.log_level) end - vim.api.nvim_set_hl(0, 'CopilotChatSpinner', { link = 'CursorColumn', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatSpinner', { link = 'DiagnosticInfo', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) vim.api.nvim_set_hl( From b7383e48a78414e8fc465ecfa55e0fa83a47b980 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 30 Nov 2024 00:27:42 +0100 Subject: [PATCH 316/338] refactor: remove redundant log messages Remove unnecessary info-level logging from agent fetching and policy enabling functions to reduce log noise. Debug information is still preserved via trace logging. --- lua/CopilotChat/copilot.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/lua/CopilotChat/copilot.lua b/lua/CopilotChat/copilot.lua index 065a3609..761aa6c5 100644 --- a/lua/CopilotChat/copilot.lua +++ b/lua/CopilotChat/copilot.lua @@ -473,7 +473,6 @@ function Copilot:fetch_agents() out['copilot'] = { name = 'Copilot', default = true, description = 'Default noop agent' } - log.info('Agents fetched') log.trace(agents) self.agents = out return out @@ -502,8 +501,6 @@ function Copilot:enable_policy(model) log.warn('Failed to enable policy for ', model, ': ', (err or response.body)) return end - - log.info('Policy enabled for ' .. model) end --- Ask a question to Copilot From 23545dc12dd6fa272eba35c88a2404a4f90a88b1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 30 Nov 2024 00:33:02 +0100 Subject: [PATCH 317/338] refactor: move source type to init.lua Move the CopilotChat.source type definition from config.lua to init.lua and update all references to use the new type location. This improves type organization by placing the core source type definition in the main module file. --- lua/CopilotChat/config.lua | 12 ++++-------- lua/CopilotChat/init.lua | 6 +++++- lua/CopilotChat/select.lua | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index d2a1974f..07385f30 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -3,14 +3,10 @@ local context = require('CopilotChat.context') local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') ---- @class CopilotChat.config.source ---- @field bufnr number ---- @field winnr number - ---@class CopilotChat.config.context ---@field description string? ----@field input fun(callback: fun(input: string?), source: CopilotChat.config.source)? ----@field resolve fun(input: string?, source: CopilotChat.config.source):table +---@field input fun(callback: fun(input: string?), source: CopilotChat.source)? +---@field resolve fun(input: string?, source: CopilotChat.source):table ---@class CopilotChat.config.prompt : CopilotChat.config.shared ---@field prompt string? @@ -59,8 +55,8 @@ local utils = require('CopilotChat.utils') ---@field context string|table|nil ---@field temperature number? ---@field headless boolean? ----@field callback fun(response: string, source: CopilotChat.config.source)? ----@field selection nil|fun(source: CopilotChat.config.source):CopilotChat.select.selection? +---@field callback fun(response: string, source: CopilotChat.source)? +---@field selection nil|fun(source: CopilotChat.source):CopilotChat.select.selection? ---@field window CopilotChat.config.window? ---@field show_help boolean? ---@field show_folds boolean? diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index b7701a57..6dcd6997 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -15,9 +15,13 @@ local M = {} local PLUGIN_NAME = 'CopilotChat' local WORD = '([^%s]+)' +--- @class CopilotChat.source +--- @field bufnr number +--- @field winnr number + --- @class CopilotChat.state --- @field copilot CopilotChat.Copilot? ---- @field source CopilotChat.config.source? +--- @field source CopilotChat.source? --- @field last_prompt string? --- @field last_response string? --- @field chat CopilotChat.ui.Chat? diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 67f5ecf1..e6b76587 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -48,7 +48,7 @@ local function get_diagnostics_in_range(bufnr, start_line, end_line) end --- Select and process current visual selection ---- @param source CopilotChat.config.source +--- @param source CopilotChat.source --- @return CopilotChat.select.selection|nil function M.visual(source) local bufnr = source.bufnr @@ -82,7 +82,7 @@ function M.visual(source) end --- Select and process whole buffer ---- @param source CopilotChat.config.source +--- @param source CopilotChat.source --- @return CopilotChat.select.selection|nil function M.buffer(source) local bufnr = source.bufnr @@ -105,7 +105,7 @@ function M.buffer(source) end --- Select and process current line ---- @param source CopilotChat.config.source +--- @param source CopilotChat.source --- @return CopilotChat.select.selection|nil function M.line(source) local bufnr = source.bufnr @@ -130,7 +130,7 @@ function M.line(source) end --- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. ---- @param source CopilotChat.config.source +--- @param source CopilotChat.source --- @return CopilotChat.select.selection|nil function M.unnamed(source) local bufnr = source.bufnr From ae5251f611188580b051c725d75f885adb9be3ff Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 30 Nov 2024 10:58:41 +0100 Subject: [PATCH 318/338] Add vid to README (#652) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c83afdc8..bad5a990 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ ![image](https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea) +https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 + > [!NOTE] > Plugin was rewritten to Lua from Python. Please check the [migration guide from version 1 to version 2](/MIGRATION.md) for more information. @@ -624,7 +626,7 @@ To chat with Copilot using the entire content of the buffer, you can add the fol } ``` -[![Chat with buffer](https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif)](https://gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0) +[![chat-with-buffer](https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif)](https://gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0)
@@ -671,7 +673,7 @@ Requires [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) plug }, ``` -![image](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b) +![telescope-integration](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b) @@ -694,7 +696,7 @@ Requires [fzf-lua](https://github.com/ibhagwan/fzf-lua) plugin to be installed. }, ``` -![image](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747) +![fzf-lua-integration](https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747) @@ -718,7 +720,7 @@ require('CopilotChat').setup({ }) ``` -![image](https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8) +![render-markdown-integration](https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8) From d0e0c83798e502c7b8d0a7030cf5d00f50b4e64e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 30 Nov 2024 09:59:01 +0000 Subject: [PATCH 319/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 0dcb07f5..2ffdf6c1 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 29 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 30 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -23,6 +23,9 @@ Table of Contents *CopilotChat-table-of-contents* |CopilotChat-| +https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 + + [!NOTE] Plugin was rewritten to Lua from Python. Please check the migration guide from version 1 to version 2 for more information. - |CopilotChat-requirements| @@ -805,11 +808,11 @@ STARGAZERS OVER TIME ~ 6. *image*: https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea 7. *@jellydn*: 8. *@deathbeam*: -9. *Chat with buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif +9. *chat-with-buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif 10. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c -11. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b -12. *image*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 -13. *image*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 +11. *telescope-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b +12. *fzf-lua-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 +13. *render-markdown-integration*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 14. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From 63f101e057b9ca4abed446f1a476b82a1e383426 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 30 Nov 2024 13:10:05 +0100 Subject: [PATCH 320/338] refactor(context): extract buffer list logic to new function Move buffer list filtering logic from config.lua to a new dedicated function `buffers()` in context.lua for better code organization and reusability. --- lua/CopilotChat/config.lua | 9 +-------- lua/CopilotChat/context.lua | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 07385f30..b3993d63 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -180,14 +180,7 @@ return { end, resolve = function(input) input = input or 'listed' - return vim.tbl_map( - context.buffer, - vim.tbl_filter(function(b) - return utils.buf_valid(b) - and vim.fn.buflisted(b) == 1 - and (input == 'listed' or #vim.fn.win_findbuf(b) > 0) - end, vim.api.nvim_list_bufs()) - ) + return context.buffers(input) end, }, file = { diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/context.lua index 2921ad86..b1232f97 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/context.lua @@ -412,6 +412,22 @@ function M.buffer(bufnr) ) end +--- Get content of all buffers +---@param buf_type string +---@return table +function M.buffers(buf_type) + async.util.scheduler() + + return vim.tbl_map( + M.buffer, + vim.tbl_filter(function(b) + return utils.buf_valid(b) + and vim.fn.buflisted(b) == 1 + and (buf_type == 'listed' or #vim.fn.win_findbuf(b) > 0) + end, vim.api.nvim_list_bufs()) + ) +end + --- Get the content of an URL ---@param url string ---@return CopilotChat.context.embed? From 96c5640b6f60e16642833899e81e7bdd73b28f87 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 1 Dec 2024 00:55:59 +0100 Subject: [PATCH 321/338] docs: update files context name in README.md Update incorrect context name from `files:content` to `files:full` in the README documentation to match the actual implementation. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bad5a990..51d2f463 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ Default contexts are: - `file` - Includes content of provided file in chat context. Supports input. - `files` - Includes all non-hidden files in the current workspace in chat context. Supports input (default list). - `files:list` - Only lists file names. - - `files:content` - Includes file content for each file found. Can be slow on large workspaces, use with care. + - `files:full` - Includes file content for each file found. Can be slow on large workspaces, use with care. - `git` - Requires `git`. Includes current git diff in chat context. Supports input (default unstaged). - `git:unstaged` - Includes unstaged changes in chat context. - `git:staged` - Includes staged changes in chat context. From f2ff63e58273ba9abf0068e987e74266750c7331 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 30 Nov 2024 23:56:59 +0000 Subject: [PATCH 322/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 2ffdf6c1..4b772f13 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -322,7 +322,7 @@ prompt by using `:` followed by the input (or pressing `complete` key after - `file` - Includes content of provided file in chat context. Supports input. - `files` - Includes all non-hidden files in the current workspace in chat context. Supports input (default list). - `files:list` - Only lists file names. - - `files:content` - Includes file content for each file found. Can be slow on large workspaces, use with care. + - `files:full` - Includes file content for each file found. Can be slow on large workspaces, use with care. - `git` - Requires `git`. Includes current git diff in chat context. Supports input (default unstaged). - `git:unstaged` - Includes unstaged changes in chat context. - `git:staged` - Includes staged changes in chat context. From b52cf127154a46961a77f17d6a179553f2c38d13 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 1 Dec 2024 01:39:27 +0100 Subject: [PATCH 323/338] docs: Improve README structure so generated vimdoc is better - panvimdoc generates help tags for h1 and h2 headers so make sure everything that should have a help tag is h1 or h2 and everything else is h3+ - remove contributors emoji so the generation is better for the help tag Signed-off-by: Tomas Slusny --- .github/workflows/ci.yml | 1 - README.md | 52 ++++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cded4653..2c4ef6ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,6 @@ jobs: with: vimdoc: CopilotChat dedupsubheadings: false - treesitter: true - uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: "chore(doc): auto generate docs" diff --git a/README.md b/README.md index 51d2f463..a6d58e5e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![pre-commit.ci](https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg)](https://results.pre-commit.ci/latest/github/CopilotC-Nvim/CopilotChat.nvim/main) [![Discord](https://img.shields.io/discord/1200633211236122665.svg)](https://discord.gg/vy6hJsTWaZ) [![Dotfyle](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat)](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim) -[![All Contributors](https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat&link=%23contributors-)](#contributors) +[![All Contributors](https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat)](#contributors) ![image](https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea) @@ -33,11 +33,11 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 - [Default configuration](#default-configuration) - [Customizing buffers](#customizing-buffers) - [Tips](#tips) -- [Roadmap (Wishlist)](#roadmap-wishlist) +- [Roadmap](#roadmap) - [Development](#development) -- [Contributors ✨](#contributors-) +- [Contributors](#contributors) -## Requirements +# Requirements - [Neovim 0.9.5+](https://neovim.io/) - Older versions are not supported, and for best compatibility 0.10.0+ is preferred - [curl](https://curl.se/) - 8.0.0+ is recommended for best compatibility. Should be installed by default on most systems and also shipped with Neovim @@ -56,7 +56,7 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 > [!WARNING] > If you are on neovim < 0.11.0, you also might want to add `noinsert` and `popup` to your `completeopt` to make the chat completion behave well. -## Installation +# Installation ### [Lazy.nvim](https://github.com/folke/lazy.nvim) @@ -122,9 +122,9 @@ require("CopilotChat").setup { See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/master/nvim/.config/nvim/lua/config/copilot.lua#L14) -## Usage +# Usage -### Commands +## Commands - `:CopilotChat ?` - Open chat window with optional input - `:CopilotChatOpen` - Open chat window @@ -139,7 +139,7 @@ See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/ma - `:CopilotChatAgents` - View and select available agents. This is reset when a new instance is made. Please set your agent in `init.lua` for persistence. - `:CopilotChat` - Ask a question with a specific prompt. For example, `:CopilotChatExplain` will ask a question with the `Explain` prompt. See [Prompts](#prompts) for more information. -### Chat Mappings +## Chat Mappings - `` - Trigger completion menu for special tokens or accept current completion (see help) - `q`/`` - Close the chat window @@ -174,7 +174,7 @@ For example, to change the submit prompt mapping: } ``` -### Prompts +## Prompts You can ask Copilot to do various tasks with prompts. You can reference prompts with `/PromptName` in chat or call with command `:CopilotChat`. Default prompts are: @@ -202,7 +202,7 @@ You can define custom prompts like this (only `prompt` is required): } ``` -### System Prompts +## System Prompts System prompts specify the behavior of the AI model. You can reference system prompts with `/PROMPT_NAME` in chat. Default system prompts are: @@ -224,7 +224,7 @@ You can define custom system prompts like this (works same as `prompts` so you c } ``` -### Sticky Prompts +## Sticky Prompts You can set sticky prompt in chat by prefixing the text with `> ` using markdown blockquote syntax. The sticky prompt will be copied at start of every new prompt in chat window. You can freely edit the sticky prompt, only rule is `> ` prefix at beginning of line. @@ -243,7 +243,7 @@ List all files in the workspace What is 1 + 11 ``` -### Models +## Models You can list available models with `:CopilotChatModels` command. Model determines the AI model used for the chat. You can set the model in the prompt by using `$` followed by the model name or default model via config using `model` key. @@ -257,7 +257,7 @@ Default models are: For more information about models, see [here](https://docs.github.com/en/copilot/using-github-copilot/asking-github-copilot-questions-in-your-ide#ai-models-for-copilot-chat) You can use more models from [here](https://github.com/marketplace/models) by using `@models` agent from [here](https://github.com/marketplace/models-github) (example: `@models Using Mistral-small, what is 1 + 11`) -### Agents +## Agents Agents are used to determine the AI agent used for the chat. You can list available agents with `:CopilotChatAgents` command. You can set the agent in the prompt by using `@` followed by the agent name or default agent via config using `agent` key. @@ -266,7 +266,7 @@ Default "noop" agent is `copilot`. For more information about extension agents, see [here](https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat) You can install more agents from [here](https://github.com/marketplace?type=apps&copilot_app=true) -### Contexts +## Contexts Contexts are used to determine the context of the chat. You can add context to the prompt by using `#` followed by the context name or default context via config using `context` (can be single or array) key. @@ -325,7 +325,7 @@ You can define custom contexts like this: What is my birthday ``` -### Selections +## Selections Selections are used to determine the source of the chat (so basically what to chat about). Selections are configurable either by default or by prompt. @@ -348,7 +348,7 @@ You can chain multiple selections like this: } ``` -### API +## API ```lua local chat = require("CopilotChat") @@ -419,9 +419,9 @@ actions.pick(actions.prompt_actions({ chat.log_level("debug") ``` -## Configuration +# Configuration -### Default configuration +## Default configuration Also see [here](/lua/CopilotChat/config.lua): @@ -478,9 +478,9 @@ Also see [here](/lua/CopilotChat/config.lua): chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - question_header = '## User ', -- Header to use for user questions - answer_header = '## Copilot ', -- Header to use for AI answers - error_header = '## Error ', -- Header to use for errors + question_header = '# User ', -- Header to use for user questions + answer_header = '# Copilot ', -- Header to use for AI answers + error_header = '# Error ', -- Header to use for errors separator = '───', -- Separator to use in chat -- default contexts @@ -585,7 +585,7 @@ Also see [here](/lua/CopilotChat/config.lua): } ``` -### Customizing buffers +## Customizing buffers You can set local options for the buffers that are created by this plugin: `copilot-diff`, `copilot-system-prompt`, `copilot-user-selection`, `copilot-chat`. @@ -603,7 +603,7 @@ vim.api.nvim_create_autocmd('BufEnter', { }) ``` -## Tips +# Tips
Quick chat with your buffer @@ -724,12 +724,12 @@ require('CopilotChat').setup({
-## Roadmap (Wishlist) +# Roadmap - Improved caching for context (persistence through restarts/smarter caching) - General QOL improvements -## Development +# Development ### Installing Pre-commit Tool @@ -741,7 +741,7 @@ make install-pre-commit This will install the pre-commit tool and the pre-commit hooks. -## Contributors ✨ +# Contributors If you want to contribute to this project, please read the [CONTRIBUTING.md](/CONTRIBUTING.md) file. From 1147f628ab98996b6d5de8c967138f06d9f027bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 1 Dec 2024 00:48:48 +0000 Subject: [PATCH 324/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 94 +++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 37 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 4b772f13..69358b5c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,18 +1,30 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 November 30 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 December 01 ============================================================================== Table of Contents *CopilotChat-table-of-contents* 1. Copilot Chat for Neovim |CopilotChat-copilot-chat-for-neovim| - - Requirements |CopilotChat-requirements| - - Installation |CopilotChat-installation| - - Usage |CopilotChat-usage| - - Configuration |CopilotChat-configuration| - - Tips |CopilotChat-tips| - - Roadmap (Wishlist) |CopilotChat-roadmap-(wishlist)| - - Development |CopilotChat-development| - - Contributors ✨ |CopilotChat-contributors-✨| -2. Links |CopilotChat-links| +2. Requirements |CopilotChat-requirements| +3. Installation |CopilotChat-installation| +4. Usage |CopilotChat-usage| + - Commands |CopilotChat-commands| + - Chat Mappings |CopilotChat-chat-mappings| + - Prompts |CopilotChat-prompts| + - System Prompts |CopilotChat-system-prompts| + - Sticky Prompts |CopilotChat-sticky-prompts| + - Models |CopilotChat-models| + - Agents |CopilotChat-agents| + - Contexts |CopilotChat-contexts| + - Selections |CopilotChat-selections| + - API |CopilotChat-api| +5. Configuration |CopilotChat-configuration| + - Default configuration |CopilotChat-default-configuration| + - Customizing buffers |CopilotChat-customizing-buffers| +6. Tips |CopilotChat-tips| +7. Roadmap |CopilotChat-roadmap| +8. Development |CopilotChat-development| +9. Contributors |CopilotChat-contributors| +10. Links |CopilotChat-links| ============================================================================== 1. Copilot Chat for Neovim *CopilotChat-copilot-chat-for-neovim* @@ -48,12 +60,13 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 - |CopilotChat-default-configuration| - |CopilotChat-customizing-buffers| - |CopilotChat-tips| -- |CopilotChat-roadmap-(wishlist)| +- |CopilotChat-roadmap| - |CopilotChat-development| -- |CopilotChat-contributors-✨| +- |CopilotChat-contributors| -REQUIREMENTS *CopilotChat-requirements* +============================================================================== +2. Requirements *CopilotChat-requirements* - Neovim 0.9.5+ - Older versions are not supported, and for best compatibility 0.10.0+ is preferred - curl - 8.0.0+ is recommended for best compatibility. Should be installed by default on most systems and also shipped with Neovim @@ -73,7 +86,8 @@ REQUIREMENTS *CopilotChat-requirements* [!WARNING] If you are on neovim < 0.11.0, you also might want to add `noinsert` and `popup` to your `completeopt` to make the chat completion behave well. -INSTALLATION *CopilotChat-installation* +============================================================================== +3. Installation *CopilotChat-installation* LAZY.NVIM ~ @@ -145,10 +159,11 @@ See @deathbeam for configuration -USAGE *CopilotChat-usage* +============================================================================== +4. Usage *CopilotChat-usage* -COMMANDS ~ +COMMANDS *CopilotChat-commands* - `:CopilotChat ?` - Open chat window with optional input - `:CopilotChatOpen` - Open chat window @@ -164,7 +179,7 @@ COMMANDS ~ - `:CopilotChat` - Ask a question with a specific prompt. For example, `:CopilotChatExplain` will ask a question with the `Explain` prompt. See |CopilotChat-prompts| for more information. -CHAT MAPPINGS ~ +CHAT MAPPINGS *CopilotChat-chat-mappings* - `` - Trigger completion menu for special tokens or accept current completion (see help) - `q`/`` - Close the chat window @@ -201,7 +216,7 @@ For example, to change the submit prompt mapping: < -PROMPTS ~ +PROMPTS *CopilotChat-prompts* You can ask Copilot to do various tasks with prompts. You can reference prompts with `/PromptName` in chat or call with command `:CopilotChat`. @@ -231,7 +246,7 @@ You can define custom prompts like this (only `prompt` is required): < -SYSTEM PROMPTS ~ +SYSTEM PROMPTS *CopilotChat-system-prompts* System prompts specify the behavior of the AI model. You can reference system prompts with `/PROMPT_NAME` in chat. Default system prompts are: @@ -255,7 +270,7 @@ can combine prompt and system prompt definitions): < -STICKY PROMPTS ~ +STICKY PROMPTS *CopilotChat-sticky-prompts* You can set sticky prompt in chat by prefixing the text with `>` using markdown blockquote syntax. The sticky prompt will be copied at start of every new @@ -276,7 +291,7 @@ and agent selection (see below). Example usage: < -MODELS ~ +MODELS *CopilotChat-models* You can list available models with `:CopilotChatModels` command. Model determines the AI model used for the chat. You can set the model in the prompt @@ -295,7 +310,7 @@ using `@models` agent from here (example: `@models Using Mistral-small, what is 1 + 11`) -AGENTS ~ +AGENTS *CopilotChat-agents* Agents are used to determine the AI agent used for the chat. You can list available agents with `:CopilotChatAgents` command. You can set the agent in @@ -308,7 +323,7 @@ You can install more agents from here -CONTEXTS ~ +CONTEXTS *CopilotChat-contexts* Contexts are used to determine the context of the chat. You can add context to the prompt by using `#` followed by the context name or default context via @@ -369,7 +384,7 @@ You can define custom contexts like this: < -SELECTIONS ~ +SELECTIONS *CopilotChat-selections* Selections are used to determine the source of the chat (so basically what to chat about). Selections are configurable either by default or by prompt. @@ -394,7 +409,7 @@ You can chain multiple selections like this: < -API ~ +API *CopilotChat-api* >lua local chat = require("CopilotChat") @@ -466,10 +481,11 @@ API ~ < -CONFIGURATION *CopilotChat-configuration* +============================================================================== +5. Configuration *CopilotChat-configuration* -DEFAULT CONFIGURATION ~ +DEFAULT CONFIGURATION *CopilotChat-default-configuration* Also see here : @@ -526,9 +542,9 @@ Also see here : chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - question_header = '## User ', -- Header to use for user questions - answer_header = '## Copilot ', -- Header to use for AI answers - error_header = '## Error ', -- Header to use for errors + question_header = '# User ', -- Header to use for user questions + answer_header = '# Copilot ', -- Header to use for AI answers + error_header = '# Error ', -- Header to use for errors separator = '───', -- Separator to use in chat -- default contexts @@ -634,7 +650,7 @@ Also see here : < -CUSTOMIZING BUFFERS ~ +CUSTOMIZING BUFFERS *CopilotChat-customizing-buffers* You can set local options for the buffers that are created by this plugin: `copilot-diff`, `copilot-system-prompt`, `copilot-user-selection`, @@ -655,7 +671,8 @@ You can set local options for the buffers that are created by this plugin: < -TIPS *CopilotChat-tips* +============================================================================== +6. Tips *CopilotChat-tips* Quick chat with your buffer ~ @@ -759,13 +776,15 @@ installed. < -ROADMAP (WISHLIST) *CopilotChat-roadmap-(wishlist)* +============================================================================== +7. Roadmap *CopilotChat-roadmap* - Improved caching for context (persistence through restarts/smarter caching) - General QOL improvements -DEVELOPMENT *CopilotChat-development* +============================================================================== +8. Development *CopilotChat-development* INSTALLING PRE-COMMIT TOOL ~ @@ -780,7 +799,8 @@ pre-commit tool: This will install the pre-commit tool and the pre-commit hooks. -CONTRIBUTORS ✨ *CopilotChat-contributors-✨* +============================================================================== +9. Contributors *CopilotChat-contributors* If you want to contribute to this project, please read the CONTRIBUTING.md file. @@ -798,13 +818,13 @@ STARGAZERS OVER TIME ~ ============================================================================== -2. Links *CopilotChat-links* +10. Links *CopilotChat-links* 1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg 2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg 3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg 4. *Dotfyle*: https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat -5. *All Contributors*: https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat&link=%23contributors- +5. *All Contributors*: https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat 6. *image*: https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea 7. *@jellydn*: 8. *@deathbeam*: From ba819627cd52129eecceaa4a7424e85e733e8207 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 1 Dec 2024 12:56:52 +0100 Subject: [PATCH 325/338] refactor(prompt): improve config handling in resolve_prompts Change resolve_prompts to handle entire config object instead of just system_prompt. This allows prompts to modify any config values, not just the system prompt, making the prompt resolution system more flexible. The change maintains backwards compatibility while enabling more advanced prompt configurations. --- lua/CopilotChat/init.lua | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 6dcd6997..7f0e4902 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -201,25 +201,24 @@ local function apply_diff(diff, config) end ---@param prompt string ----@param system_prompt string? ----@return string, string -local function resolve_prompts(prompt, system_prompt) +---@param config CopilotChat.config.shared +---@return string, CopilotChat.config +local function resolve_prompts(prompt, config) local prompts_to_use = M.prompts() local depth = 0 local MAX_DEPTH = 10 - local function resolve(inner_prompt, inner_system_prompt) + local function resolve(inner_prompt, inner_config) if depth >= MAX_DEPTH then - return inner_prompt, inner_system_prompt + return inner_prompt, inner_config end depth = depth + 1 inner_prompt = string.gsub(inner_prompt, '/' .. WORD, function(match) local p = prompts_to_use[match] if p then - local resolved_prompt, resolved_system_prompt = - resolve(p.prompt or '', p.system_prompt or inner_system_prompt) - inner_system_prompt = resolved_system_prompt + local resolved_prompt, resolved_config = resolve(p.prompt or '', p) + inner_config = vim.tbl_deep_extend('force', inner_config, resolved_config) return resolved_prompt end @@ -227,10 +226,10 @@ local function resolve_prompts(prompt, system_prompt) end) depth = depth - 1 - return inner_prompt, inner_system_prompt + return inner_prompt, inner_config end - return resolve(prompt, system_prompt) + return resolve(prompt, config) end ---@param prompt string @@ -621,6 +620,7 @@ function M.toggle(config) end end +--- Get the last response. --- @returns string function M.response() return state.last_response @@ -702,13 +702,14 @@ function M.ask(prompt, config) end -- Resolve prompt references - local resolved_prompt, system_prompt = resolve_prompts(prompt, config.system_prompt) + local prompt, config = resolve_prompts(prompt, config) + local system_prompt = config.system_prompt -- Remove sticky prefix prompt = vim.trim(table.concat( vim.tbl_map(function(l) return l:gsub('^>%s+', '') - end, vim.split(resolved_prompt, '\n')), + end, vim.split(prompt, '\n')), '\n' )) @@ -1165,8 +1166,8 @@ function M.setup(config) end local lines = {} - local config = state.chat.config - local prompt, system_prompt = resolve_prompts(section.content, config.system_prompt) + local prompt, config = resolve_prompts(section.content, state.chat.config) + local system_prompt = config.system_prompt async.run(function() local selected_agent = resolve_agent(prompt, config) From f8942dae450779f1e95614c7ffdeed5361c0e5ed Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 1 Dec 2024 13:34:19 +0100 Subject: [PATCH 326/338] docs: add config retrieval example to README Add example showing how to access the current chat configuration and print the model name, helping users understand how to retrieve configuration values at runtime. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index a6d58e5e..32dd1796 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,10 @@ local prompts = chat.prompts() -- Get last copilot response (also can be used for integrations and custom keymaps) local response = chat.response() +-- Retrieve current chat config +local config = chat.config +print(config.model) + -- Pick a prompt using vim.ui.select local actions = require("CopilotChat.actions") From 150989ce015424f8a4994664188ddab947450011 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 1 Dec 2024 12:35:09 +0000 Subject: [PATCH 327/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 69358b5c..a686d57f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -468,6 +468,10 @@ API *CopilotChat-api* -- Get last copilot response (also can be used for integrations and custom keymaps) local response = chat.response() + -- Retrieve current chat config + local config = chat.config + print(config.model) + -- Pick a prompt using vim.ui.select local actions = require("CopilotChat.actions") From d1b5f73a9fdc53609f524ffac71c5cddcd80737b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 1 Dec 2024 14:40:24 +0100 Subject: [PATCH 328/338] docs: Update badges and remove migration guide mention as its old (#660) * docs: Update badges * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 32dd1796..5cd7ade2 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ # Copilot Chat for Neovim -[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg)](https://copilotc-nvim.github.io/CopilotChat.nvim/) -[![pre-commit.ci](https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg)](https://results.pre-commit.ci/latest/github/CopilotC-Nvim/CopilotChat.nvim/main) -[![Discord](https://img.shields.io/discord/1200633211236122665.svg)](https://discord.gg/vy6hJsTWaZ) -[![Dotfyle](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat)](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim) -[![All Contributors](https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat)](#contributors) +[![Release](https://img.shields.io/github/v/release/CopilotC-Nvim/CopilotChat.nvim?logo=github&style=flat-square)](https://github.com/CopilotC-Nvim/CopilotChat.nvim/releases/latest) +[![Build](https://img.shields.io/github/actions/workflow/status/CopilotC-Nvim/CopilotChat.nvim/ci.yml?logo=github&style=flat-square)](/.github/workflows/ci.yml) +[![Contributors](https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&logo=github&label=contributors&style=flat-square)](#contributors) +[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg?logo=vim&style=flat-square)](/doc/CopilotChat.txt) +[![Discord](https://img.shields.io/discord/1200633211236122665?logo=discord&label=discord&style=flat-square)](https://discord.gg/vy6hJsTWaZ) +[![Dotfyle](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat-square)](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim) ![image](https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea) https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 -> [!NOTE] -> Plugin was rewritten to Lua from Python. Please check the [migration guide from version 1 to version 2](/MIGRATION.md) for more information. - - [Requirements](#requirements) - [Installation](#installation) - [Lazy.nvim](#lazynvim) From 84e7a2fa624dc82bd9094230eb12429881576734 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 1 Dec 2024 13:40:48 +0000 Subject: [PATCH 329/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index a686d57f..bb3f3d7e 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -29,17 +29,14 @@ Table of Contents *CopilotChat-table-of-contents* ============================================================================== 1. Copilot Chat for Neovim *CopilotChat-copilot-chat-for-neovim* - - + + |CopilotChat-| - |CopilotChat-| + https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 - - [!NOTE] Plugin was rewritten to Lua from Python. Please check the migration - guide from version 1 to version 2 for more information. - |CopilotChat-requirements| - |CopilotChat-installation| - |CopilotChat-lazy.nvim| @@ -824,20 +821,21 @@ STARGAZERS OVER TIME ~ ============================================================================== 10. Links *CopilotChat-links* -1. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg -2. *pre-commit.ci*: https://results.pre-commit.ci/badge/github/CopilotC-Nvim/CopilotChat.nvim/main.svg -3. *Discord*: https://img.shields.io/discord/1200633211236122665.svg -4. *Dotfyle*: https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat -5. *All Contributors*: https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&style=flat -6. *image*: https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea -7. *@jellydn*: -8. *@deathbeam*: -9. *chat-with-buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif -10. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c -11. *telescope-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b -12. *fzf-lua-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 -13. *render-markdown-integration*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 -14. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg +1. *Release*: https://img.shields.io/github/v/release/CopilotC-Nvim/CopilotChat.nvim?logo=github&style=flat-square +2. *Build*: https://img.shields.io/github/actions/workflow/status/CopilotC-Nvim/CopilotChat.nvim/ci.yml?logo=github&style=flat-square +3. *Contributors*: https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&logo=github&label=contributors&style=flat-square +4. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg?logo=vim&style=flat-square +5. *Discord*: https://img.shields.io/discord/1200633211236122665?logo=discord&label=discord&style=flat-square +6. *Dotfyle*: https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat-square +7. *image*: https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea +8. *@jellydn*: +9. *@deathbeam*: +10. *chat-with-buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif +11. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c +12. *telescope-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b +13. *fzf-lua-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 +14. *render-markdown-integration*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 +15. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From c8a6632ed9efa56569f92524b28df2fcb6f816fd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 1 Dec 2024 17:10:18 +0100 Subject: [PATCH 330/338] docs: cleanup some vimdoc issues in README Signed-off-by: Tomas Slusny --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5cd7ade2..8be121cd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ +
+ # Copilot Chat for Neovim -[![Release](https://img.shields.io/github/v/release/CopilotC-Nvim/CopilotChat.nvim?logo=github&style=flat-square)](https://github.com/CopilotC-Nvim/CopilotChat.nvim/releases/latest) -[![Build](https://img.shields.io/github/actions/workflow/status/CopilotC-Nvim/CopilotChat.nvim/ci.yml?logo=github&style=flat-square)](/.github/workflows/ci.yml) -[![Contributors](https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&logo=github&label=contributors&style=flat-square)](#contributors) -[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg?logo=vim&style=flat-square)](/doc/CopilotChat.txt) -[![Discord](https://img.shields.io/discord/1200633211236122665?logo=discord&label=discord&style=flat-square)](https://discord.gg/vy6hJsTWaZ) -[![Dotfyle](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat-square)](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim) +[![Release](https://img.shields.io/github/v/release/CopilotC-Nvim/CopilotChat.nvim?logo=github&style=for-the-badge)](https://github.com/CopilotC-Nvim/CopilotChat.nvim/releases/latest) +[![Build](https://img.shields.io/github/actions/workflow/status/CopilotC-Nvim/CopilotChat.nvim/ci.yml?logo=github&style=for-the-badge)](https://github.com/CopilotC-Nvim/CopilotChat.nvim/.github/workflows/ci.yml) +[![Contributors](https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&logo=github&label=contributors&style=for-the-badge)](#contributors) +[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg?logo=vim&style=for-the-badge)](https://github.com/CopilotC-Nvim/CopilotChat.nvim/doc/CopilotChat.txt) +[![Discord](https://img.shields.io/discord/1200633211236122665?logo=discord&label=discord&style=for-the-badge)](https://discord.gg/vy6hJsTWaZ) +[![Dotfyle](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=for-the-badge)](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim) + +
![image](https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea) @@ -76,7 +80,7 @@ return { } ``` -See @jellydn for [configuration](https://github.com/jellydn/lazy-nvim-ide/blob/main/lua/plugins/extras/copilot-chat-v2.lua) +See [@jellydn](https://github.com/jellydn) for [configuration](https://github.com/jellydn/lazy-nvim-ide/blob/main/lua/plugins/extras/copilot-chat-v2.lua) ### [Vim-Plug](https://github.com/junegunn/vim-plug) @@ -118,7 +122,7 @@ require("CopilotChat").setup { } ``` -See @deathbeam for [configuration](https://github.com/deathbeam/dotfiles/blob/master/nvim/.config/nvim/lua/config/copilot.lua#L14) +See [@deathbeam](https://github.com/deathbeam) for [configuration](https://github.com/deathbeam/dotfiles/blob/master/nvim/.config/nvim/lua/config/copilot.lua) # Usage From 9c2fcb41fbc25c86bf9d489aa4fa7a5bbf0f56f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 1 Dec 2024 16:17:36 +0000 Subject: [PATCH 331/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 77 ++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 46 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index bb3f3d7e..ea933eac 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -3,10 +3,9 @@ ============================================================================== Table of Contents *CopilotChat-table-of-contents* -1. Copilot Chat for Neovim |CopilotChat-copilot-chat-for-neovim| -2. Requirements |CopilotChat-requirements| -3. Installation |CopilotChat-installation| -4. Usage |CopilotChat-usage| +1. Requirements |CopilotChat-requirements| +2. Installation |CopilotChat-installation| +3. Usage |CopilotChat-usage| - Commands |CopilotChat-commands| - Chat Mappings |CopilotChat-chat-mappings| - Prompts |CopilotChat-prompts| @@ -17,22 +16,14 @@ Table of Contents *CopilotChat-table-of-contents* - Contexts |CopilotChat-contexts| - Selections |CopilotChat-selections| - API |CopilotChat-api| -5. Configuration |CopilotChat-configuration| +4. Configuration |CopilotChat-configuration| - Default configuration |CopilotChat-default-configuration| - Customizing buffers |CopilotChat-customizing-buffers| -6. Tips |CopilotChat-tips| -7. Roadmap |CopilotChat-roadmap| -8. Development |CopilotChat-development| -9. Contributors |CopilotChat-contributors| -10. Links |CopilotChat-links| - -============================================================================== -1. Copilot Chat for Neovim *CopilotChat-copilot-chat-for-neovim* - - - |CopilotChat-| - - +5. Tips |CopilotChat-tips| +6. Roadmap |CopilotChat-roadmap| +7. Development |CopilotChat-development| +8. Contributors |CopilotChat-contributors| +9. Links |CopilotChat-links| https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 @@ -63,7 +54,7 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 ============================================================================== -2. Requirements *CopilotChat-requirements* +1. Requirements *CopilotChat-requirements* - Neovim 0.9.5+ - Older versions are not supported, and for best compatibility 0.10.0+ is preferred - curl - 8.0.0+ is recommended for best compatibility. Should be installed by default on most systems and also shipped with Neovim @@ -84,7 +75,7 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 and `popup` to your `completeopt` to make the chat completion behave well. ============================================================================== -3. Installation *CopilotChat-installation* +2. Installation *CopilotChat-installation* LAZY.NVIM ~ @@ -107,7 +98,7 @@ LAZY.NVIM ~ } < -See @jellydn for configuration +See @jellydn for configuration @@ -152,12 +143,12 @@ MANUAL ~ } < -See @deathbeam for configuration - +See @deathbeam for configuration + ============================================================================== -4. Usage *CopilotChat-usage* +3. Usage *CopilotChat-usage* COMMANDS *CopilotChat-commands* @@ -483,7 +474,7 @@ API *CopilotChat-api* ============================================================================== -5. Configuration *CopilotChat-configuration* +4. Configuration *CopilotChat-configuration* DEFAULT CONFIGURATION *CopilotChat-default-configuration* @@ -673,7 +664,7 @@ You can set local options for the buffers that are created by this plugin: ============================================================================== -6. Tips *CopilotChat-tips* +5. Tips *CopilotChat-tips* Quick chat with your buffer ~ @@ -778,14 +769,14 @@ installed. ============================================================================== -7. Roadmap *CopilotChat-roadmap* +6. Roadmap *CopilotChat-roadmap* - Improved caching for context (persistence through restarts/smarter caching) - General QOL improvements ============================================================================== -8. Development *CopilotChat-development* +7. Development *CopilotChat-development* INSTALLING PRE-COMMIT TOOL ~ @@ -801,7 +792,7 @@ This will install the pre-commit tool and the pre-commit hooks. ============================================================================== -9. Contributors *CopilotChat-contributors* +8. Contributors *CopilotChat-contributors* If you want to contribute to this project, please read the CONTRIBUTING.md file. @@ -819,23 +810,17 @@ STARGAZERS OVER TIME ~ ============================================================================== -10. Links *CopilotChat-links* - -1. *Release*: https://img.shields.io/github/v/release/CopilotC-Nvim/CopilotChat.nvim?logo=github&style=flat-square -2. *Build*: https://img.shields.io/github/actions/workflow/status/CopilotC-Nvim/CopilotChat.nvim/ci.yml?logo=github&style=flat-square -3. *Contributors*: https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&logo=github&label=contributors&style=flat-square -4. *Documentation*: https://img.shields.io/badge/documentation-yes-brightgreen.svg?logo=vim&style=flat-square -5. *Discord*: https://img.shields.io/discord/1200633211236122665?logo=discord&label=discord&style=flat-square -6. *Dotfyle*: https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=flat-square -7. *image*: https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea -8. *@jellydn*: -9. *@deathbeam*: -10. *chat-with-buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif -11. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c -12. *telescope-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b -13. *fzf-lua-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 -14. *render-markdown-integration*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 -15. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg +9. Links *CopilotChat-links* + +1. *image*: https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea +2. *@jellydn*: +3. *@deathbeam*: +4. *chat-with-buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif +5. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c +6. *telescope-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b +7. *fzf-lua-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 +8. *render-markdown-integration*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 +9. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From 101435206fbc8f909378e9c881f832ccc9f878a2 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 1 Dec 2024 17:20:53 +0100 Subject: [PATCH 332/338] fix: correct badge links in README.md Update GitHub Actions workflow and documentation links to their correct paths. This ensures that the badge links in README.md point to the proper destinations. - Fix GitHub Actions workflow badge link - Fix documentation badge link to use relative path --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8be121cd..bb7ea54b 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ # Copilot Chat for Neovim [![Release](https://img.shields.io/github/v/release/CopilotC-Nvim/CopilotChat.nvim?logo=github&style=for-the-badge)](https://github.com/CopilotC-Nvim/CopilotChat.nvim/releases/latest) -[![Build](https://img.shields.io/github/actions/workflow/status/CopilotC-Nvim/CopilotChat.nvim/ci.yml?logo=github&style=for-the-badge)](https://github.com/CopilotC-Nvim/CopilotChat.nvim/.github/workflows/ci.yml) +[![Build](https://img.shields.io/github/actions/workflow/status/CopilotC-Nvim/CopilotChat.nvim/ci.yml?logo=github&style=for-the-badge)](https://github.com/CopilotC-Nvim/CopilotChat.nvim/actions/workflows/ci.yml) [![Contributors](https://img.shields.io/github/all-contributors/CopilotC-Nvim/CopilotChat.nvim?color=ee8449&logo=github&label=contributors&style=for-the-badge)](#contributors) -[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg?logo=vim&style=for-the-badge)](https://github.com/CopilotC-Nvim/CopilotChat.nvim/doc/CopilotChat.txt) +[![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg?logo=vim&style=for-the-badge)](/doc/CopilotChat.txt) [![Discord](https://img.shields.io/discord/1200633211236122665?logo=discord&label=discord&style=for-the-badge)](https://discord.gg/vy6hJsTWaZ) [![Dotfyle](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=for-the-badge)](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim) From 8a2897a27f000ca7e19f7be99efddf724a8a6de9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 1 Dec 2024 18:25:15 +0100 Subject: [PATCH 333/338] docs: Remove toc Signed-off-by: Tomas Slusny --- README.md | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/README.md b/README.md index bb7ea54b..d5cbd0c0 100644 --- a/README.md +++ b/README.md @@ -9,35 +9,11 @@ [![Discord](https://img.shields.io/discord/1200633211236122665?logo=discord&label=discord&style=for-the-badge)](https://discord.gg/vy6hJsTWaZ) [![Dotfyle](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim/shield?style=for-the-badge)](https://dotfyle.com/plugins/CopilotC-Nvim/CopilotChat.nvim) - - ![image](https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea) https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 -- [Requirements](#requirements) -- [Installation](#installation) - - [Lazy.nvim](#lazynvim) - - [Vim-Plug](#vim-plug) - - [Manual](#manual) -- [Usage](#usage) - - [Commands](#commands) - - [Chat Mappings](#chat-mappings) - - [Prompts](#prompts) - - [System Prompts](#system-prompts) - - [Sticky Prompts](#sticky-prompts) - - [Models](#models) - - [Agents](#agents) - - [Contexts](#contexts) - - [Selections](#selections) - - [API](#api) -- [Configuration](#configuration) - - [Default configuration](#default-configuration) - - [Customizing buffers](#customizing-buffers) -- [Tips](#tips) -- [Roadmap](#roadmap) -- [Development](#development) -- [Contributors](#contributors) + # Requirements From 3b6f193849481e56c42da3c52c4ce620a9d5cd0b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 1 Dec 2024 17:26:46 +0000 Subject: [PATCH 334/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 44 ++++++++------------------------------------ 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ea933eac..676b9c62 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -26,33 +26,6 @@ Table of Contents *CopilotChat-table-of-contents* 9. Links |CopilotChat-links| -https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 - -- |CopilotChat-requirements| -- |CopilotChat-installation| - - |CopilotChat-lazy.nvim| - - |CopilotChat-vim-plug| - - |CopilotChat-manual| -- |CopilotChat-usage| - - |CopilotChat-commands| - - |CopilotChat-chat-mappings| - - |CopilotChat-prompts| - - |CopilotChat-system-prompts| - - |CopilotChat-sticky-prompts| - - |CopilotChat-models| - - |CopilotChat-agents| - - |CopilotChat-contexts| - - |CopilotChat-selections| - - |CopilotChat-api| -- |CopilotChat-configuration| - - |CopilotChat-default-configuration| - - |CopilotChat-customizing-buffers| -- |CopilotChat-tips| -- |CopilotChat-roadmap| -- |CopilotChat-development| -- |CopilotChat-contributors| - - ============================================================================== 1. Requirements *CopilotChat-requirements* @@ -812,15 +785,14 @@ STARGAZERS OVER TIME ~ ============================================================================== 9. Links *CopilotChat-links* -1. *image*: https://github.com/user-attachments/assets/9ee30811-0fb8-4500-91f6-34ea6b26adea -2. *@jellydn*: -3. *@deathbeam*: -4. *chat-with-buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif -5. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c -6. *telescope-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b -7. *fzf-lua-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 -8. *render-markdown-integration*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 -9. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg +1. *@jellydn*: +2. *@deathbeam*: +3. *chat-with-buffer*: https://i.gyazo.com/9b8cbf1d78a19f326282a6520bc9aab0.gif +4. *inline-chat*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/608e3c9b-8569-408d-a5d1-2213325fc93c +5. *telescope-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/14360883-7535-4ee3-aca1-79f6c39f626b +6. *fzf-lua-integration*: https://github.com/CopilotC-Nvim/CopilotChat.nvim/assets/5115805/743455bb-9517-48a8-a7a1-81215dc3b747 +7. *render-markdown-integration*: https://github.com/user-attachments/assets/d8dc16f8-3f61-43fa-bfb9-83f240ae30e8 +8. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg Generated by panvimdoc From 6c7a6871e32bbd7afdc01597fc5605d8991bfbc3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sun, 1 Dec 2024 23:47:12 +0100 Subject: [PATCH 335/338] fix: only log debug messages when data exists Previously debug messages were always logged even with empty data. This change adds a condition to only log debug messages when data exists and is not empty, reducing noise in the debug output. --- lua/CopilotChat/notify.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/notify.lua b/lua/CopilotChat/notify.lua index fd04a97f..db1af837 100644 --- a/lua/CopilotChat/notify.lua +++ b/lua/CopilotChat/notify.lua @@ -11,7 +11,10 @@ M.listeners = {} ---@param data any function M.publish(event_name, data) if M.listeners[event_name] then - log.debug(event_name .. ':', data) + if data and data ~= '' then + log.debug(event_name .. ':', data) + end + for _, callback in ipairs(M.listeners[event_name]) do callback(data) end From e1e1a54c69eed2fed9baf7fadb05f791ce3f5939 Mon Sep 17 00:00:00 2001 From: "Dung Duc Huynh (Kaka)" <870029+jellydn@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:27:26 +0800 Subject: [PATCH 336/338] docs: remove canary branch --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5cbd0c0..ac358bcb 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 return { { "CopilotC-Nvim/CopilotChat.nvim", - branch = "canary", dependencies = { { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim" }, -- for curl, log wrapper @@ -66,7 +65,7 @@ Similar to the lazy setup, you can use the following configuration: call plug#begin() Plug 'github/copilot.vim' Plug 'nvim-lua/plenary.nvim' -Plug 'CopilotC-Nvim/CopilotChat.nvim', { 'branch': 'canary' } +Plug 'CopilotC-Nvim/CopilotChat.nvim', { 'branch': 'main' } call plug#end() lua << EOF @@ -87,7 +86,7 @@ cd ~/.config/nvim/pack/copilotchat/start git clone https://github.com/github/copilot.vim git clone https://github.com/nvim-lua/plenary.nvim -git clone -b canary https://github.com/CopilotC-Nvim/CopilotChat.nvim +git clone -b main https://github.com/CopilotC-Nvim/CopilotChat.nvim ``` 2. Add to your configuration (e.g. `~/.config/nvim/init.lua`) From 71459105476cd3652de90fa0490cea044869c9ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 2 Dec 2024 07:09:45 +0000 Subject: [PATCH 337/338] chore(doc): auto generate docs --- doc/CopilotChat.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 676b9c62..be4de4f7 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 December 01 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2024 December 02 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -57,7 +57,6 @@ LAZY.NVIM ~ return { { "CopilotC-Nvim/CopilotChat.nvim", - branch = "canary", dependencies = { { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim" }, -- for curl, log wrapper @@ -83,7 +82,7 @@ Similar to the lazy setup, you can use the following configuration: call plug#begin() Plug 'github/copilot.vim' Plug 'nvim-lua/plenary.nvim' - Plug 'CopilotC-Nvim/CopilotChat.nvim', { 'branch': 'canary' } + Plug 'CopilotC-Nvim/CopilotChat.nvim', { 'branch': 'main' } call plug#end() lua << EOF @@ -105,7 +104,7 @@ MANUAL ~ git clone https://github.com/github/copilot.vim git clone https://github.com/nvim-lua/plenary.nvim - git clone -b canary https://github.com/CopilotC-Nvim/CopilotChat.nvim + git clone -b main https://github.com/CopilotC-Nvim/CopilotChat.nvim < 1. Add to your configuration (e.g. `~/.config/nvim/init.lua`) From 451d365928a994cda3505a84905303f790e28df8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 2 Dec 2024 09:32:36 +0100 Subject: [PATCH 338/338] Add deprecation notice to canary branch Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 7f0e4902..8017bbd1 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -875,6 +875,8 @@ end --- Set up the plugin ---@param config CopilotChat.config? function M.setup(config) + utils.deprecate("'canary' branch", "'main' branch") + -- Handle changed configuration if config then if config.mappings then